Microwave filters GUI  2.0.3
mwfiltersgui.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 ## @file
5 ## @todo Linux executables created with cx_freeze report sys.stdout.encoding equal to None. To avoid program crash when trying to write non-ascii characters, these characters are replaced by '?'. It would be better to enforce writting uft-8 to stdout regardless of sys.stdout.encoding .
6 ## @todo Possibility to change styles with a combobox. Not obvious in pyQt.
7 ##@todo Check end-of-line in a Mac computer. It should work if platform.system() returns something different than 'Windows', 'Microsoft', 'Vista' or 'Linux'.
8 
9 ##
10 # @package mwfiltersgui Graphical User Interface
11 # <p>@author J.M. Rius, J. Mateu, J.M. Tamayo, C. Collado, A. Padilla and J. O'Callaghan.
12 # <p>Dpt. Signal Tehory and Communications, Universitat Politècnica de Catalunya (UPC),
13 # @version 2.0.3
14 # @date June 11, 2013
15 # <p>Acknowledgement: The software was developed in the frame of contract 21398/08/NL/GLC with the European Space Agency (ESA). Technical Offer was Christoph Ernst.
16 # Further features were developed under contract UPC-C7767 with Thales Alenia Space España (TAS-E).
17 # Contributions to the definition of the software functionality and testing have been made by Christoph Ernst, Mónica Martínez Mendoza and other
18 # ESA-ESTEC personnel, and Santiago Sobrino and Luis Roglá from TAS-E.
19 # <p>Copyright: &copy; 2009 Universitat Politècnica de Catalunya (UPC). <br>Contact: lossyfilters@tsc.upc.edu
20 # <p>This program is free software; you can redistribute it and/or modify it under the terms of
21 # the GNU General Public License as published by the Free Software Foundation; either
22 # version 3 of the License, or (at your option) any later version.
23 # <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
24 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
25 # See the GNU General Public License for more details.
26 # <p>You should have received a copy of the GNU General Public License along with this program
27 # in file "LICENSE.GPL3"; if not, download it from http://www.gnu.org/licenses/gpl-3.0.html .
28 # <p>Additional permission under GNU GPL version 3 section 7:
29 # <p>If you modify this Program, or any covered work, by linking or combining it with the "Extra Filters Library" (libextrafilters),
30 # the "Lossy Filters Library" (liblossyfilters) or the "License Check Library" (libchecklicense), or modified versions of that libraries,
31 # containing parts covered by the terms described in files LICENSE.LIBEXTRAFILTERS and LICENSE.LIBLOSSYFILTERS,
32 # the licensors of this Program grant you additional permission to convey the resulting work.
33 # <p>Lossyfilters software GUI uses the open-source dbplot.py module, which is based in part on the work of the Qwt project
34 # (http://qwt.sf.net) and has been released by UPC under the terms of the GNU GPL version 3.
35 #
36 
37 import sys
38 import getopt
39 import os.path
40 import platform
41 import codecs
42 import types
43 import copy
44 from math import log10, ceil, pi, sin, sqrt
45 from functools import partial
46 from PyQt4.QtCore import *
47 from PyQt4.QtGui import *
48 import PyQt4.Qwt5 as Qwt
49 from dbplot import DbPlot
50 import numpy as np
51 import scipy as sc
52 from scipy import interpolate
53 import Ui_mainwindow
54 import Ui_parameditdlg
55 import Ui_couplingmatrixdlg
56 import Ui_matrixeditdlg
57 import Ui_tempvariationdlg
58 
59 ##
60 # Global variable useful only for Mac OS X
61 MAC = "qt_mac_set_native_menubar" in dir()
62 
63 from libcommonfunc import complexStr, listStr, myPrint, setGlobalVariablesLibCommonFunc, Parameters, readParamFile, printHeader, Sparameters, CouplingMatrices, FrequencyTransformBP, parseError, synthError, licenseError, criticalErrorMsg, warningMsg, saveSignificantDigits, saveSignificantDigitsEnergy
64 from libfreefilters import CharPolys, setGlobalVariablesLibFreeFilters #, importedExtraFilters
65 
66 # Try to import the libextrafilters. If it is not available, set importedExtraFilters flag to False.
67 try:
68  if os.path.exists('libextrafilters.pyc'): existingExtraFilters = True
69  else: existingExtraFilters = False
70  from libextrafilters import TZoptimMask
71 except ImportError:
72  ##
73  # Variable set to True if the non-free libextrafilters is available. Otherwise set to False.
74  importedExtraFilters = False
75 else:
76  importedExtraFilters = True
77 
78 # Try to import the liblossyfilters. If it is not available, set importedLossyFilters flag to False.
79 try:
80  if os.path.exists('liblossyfilters.pyc'): existingLossyFilters = True
81  else: existingLossyFilters = False
82  from liblossyfilters import predistortion, nonUniformQ, importedCheckLicense, existingCheckLicense
83 except ImportError:
84  ##
85  # Variable set to True if the non-free liblossyfilters is available. Otherwise set to False.
86  importedLossyFilters = False
87 else:
88  importedLossyFilters = True
89 
90 
91 ##
92 #
93 # Class derived from DbPlot to allow adding some widgets for plotting energy and power.
94 #
96 # def __init__(self, XData, leftYData, rightXData = None, rightYData = None, leftWidth = None, rightWidth = None, leftYmin = None, leftYmax = None, rightYmin = None, rightYmax = None, Xmin = None, Xmax = None, windowTitle = None, tabTitle = None, title=None, leftYNames=None, rightYNames=None, leftYVisible=None, rightYVisible=None, XLabel=None, leftYLabel=None, rightYLabel=None, leftYColors=None, rightYColors=None, textLabel=None, Xunits=None, leftYunits=None, rightYunits = None, parent=None):
97  ##
98  #
99  # Derived class constructor.
100  #
101  def __init__(self, *posPars, **keyPars):
102  super(EnergyDbPlot, self).__init__(*posPars, **keyPars)
103 
104  horLayout = QHBoxLayout()
105  horLayout.addStretch()
106  horLayout.addWidget(QLabel("Excitation:"))
107  self.excitation_comboBox = QComboBox()
108  self.excitation_comboBox.addItems(['Direct (S->L)', 'Reverse (L->S)'])
109  horLayout.addWidget(self.excitation_comboBox)
110  horLayout.addSpacing(20)
111  horLayout.addWidget(QLabel("Input power:"))
112  self.inputPower_doubleSpinBox = QDoubleSpinBox()
113  self.inputPower_doubleSpinBox.setRange(0, 1e9)
114  self.inputPower_doubleSpinBox.setValue(1)
115  horLayout.addWidget(self.inputPower_doubleSpinBox)
116  horLayout.addWidget(QLabel("W"))
117 
118 # horLayout.addStretch()
119 # horLayout.addWidget(QLabel("Input power:"))
120 # self.inputPower_lineEdit = QLineEdit('1')
121 # realPositiveValidator = QRegExpValidator( reRealPositive, self)
122 # self.inputPower_lineEdit.setValidator(realPositiveValidator)
123 # horLayout.addWidget(self.inputPower_lineEdit)
124 # horLayout.addWidget(QLabel("W"))
125 
126  self.verticalLayout.insertLayout(0, horLayout)
127 
128  # Connect inputPower and excitation widgets to slot
129  self.connect(self.inputPower_doubleSpinBox, SIGNAL("valueChanged(double)"), self.updateInputPower)
130  self.connect(self.excitation_comboBox, SIGNAL("currentIndexChanged(int)"), self.updateExcitation)
131 
132  ##
133  # List of leftYData for reverse excitation. List for all Tabs.
134  self.leftYDataRev = []
135 
136  ##
137  # List of rightYData for reverse excitation. List for all Tabs.
138  self.rightYDataRev = []
139 
140 
141  ##
142  #
143  # Update energy and power plots after inputPower value has been changed by the user.
144  # Y-axis are autoscaled after update.
145  #
146  def updateInputPower(self, value):
147 
148  for nTab in range(self.Ntabs):
149  newLeftYData = [ value*data for data in np.array(self.leftYData[nTab]) ]
150  newRightYData = [ value*data for data in self.rightYData[nTab] ]if self.rightYData[nTab] is not None else None
151 
152  self.update(nTab, XData = self.XData[nTab], rightXData = self.rightXData[nTab], leftYData = newLeftYData, rightYData = newRightYData )
153  self.autoScaleSomeAxis(nTab, autoScaleBottomX = False, autoScaleLeftY = True, autoScaleRightY = True)
154 
155 
156  ##
157  #
158  # Update energy and power plots after excitation value has been changed by the user.
159  # Y-axis are autoscaled after update.
160  #
161  def updateExcitation(self, value):
162 
163  for nTab in range(self.Ntabs):
164  if value == 0:
165  newLeftYData = self.leftYData[nTab]
166  newRightYData = self.rightYData[nTab]
167  elif value == 1:
168  newLeftYData = self.leftYDataRev[nTab]
169  newRightYData = self.rightYDataRev[nTab]
170 
171  self.update(nTab, XData = self.XData[nTab], rightXData = self.rightXData[nTab], leftYData = newLeftYData, rightYData = newRightYData )
172  self.autoScaleSomeAxis(nTab, autoScaleBottomX = False, autoScaleLeftY = True, autoScaleRightY = True)
173 
174 
175  ##
176  #
177  # Set YData for reverse excitation.
178  # This method appends data to a list for all tabs, so it must be called for all tabs sequentially in tab order.
179  # @param leftYDataRev = Left-y data for reverse excitation.
180  # @param rightYDataRev = Right-y data for reverse excitation.
181  #
182  def appendReverseData(self, leftYDataRev, rightYDataRev):
183  self.leftYDataRev.append(leftYDataRev)
184  self.rightYDataRev.append(rightYDataRev)
185 
186 
187  ##
188  #
189  # Replace YData for reverse excitation.
190  # @param nTab = Tab index.
191  # @param leftYDataRev = Left-y data for reverse excitation.
192  # @param rightYDataRev = Right-y data for reverse excitation.
193  #
194  def replaceReverseData(self, nTab, leftYDataRev, rightYDataRev):
195  self.leftYDataRev[nTab] = leftYDataRev
196  self.rightYDataRev[nTab] = rightYDataRev
197 
198  # Show the direct or reverse data depending on the excitation widget status
199  self.updateExcitation(self.excitation_comboBox.currentIndex())
200 
201 
202 ##
203 #
204 # Widget to ask the user for temperature variation parameters.
205 #
206 class TempVarDlg(QDialog, Ui_tempvariationdlg.Ui_TempVarDlg):
207 
208  def __init__(self, parent=None):
209  super(TempVarDlg, self).__init__(parent)
210 # self.setAttribute(Qt.WA_DeleteOnClose) # Do not set Qt.WA_DeleteOnClose in modal dump dialogs: you would not be able to read the data after accepting the dialog
211  self.setupUi(self)
212 
213 
214 ##
215 #
216 # Specification mask class.
217 #
218 class SpecMask(object):
219 
220  ##
221  #
222  # Read specification mask from file.
223  # @param fileName = File Name
224  # @param parent = Parent window for the temperature variation dialog
225  #
226  def __init__(self, fileName, parent=None):
227 
228  # Start helper functions
229 
230  ##
231  #
232  # Open a widget to get temperature variation parameters:
233  # Tamb = Ambient Temperature: Temperature at which the filter is built. At ambient temperature, the measured [S] parameters correspond to the designed [S] parameters.
234  # Tmax = Maximum operating temperature. Due to thermal expansion, the filter at Tmax is actually larger than the filter built at Tamb and the [S] parameters are shifted to the left (to lower frequencies).
235  # Tmin = Minimum operating temperature. Due to thermal expansion, the filter at Tmin is actually smaller than the filter built at Tamb and the [S] parameters are shifted to the right (to higher frequencies).
236  # alpha = Thermal expansion coefficient, expressed in parts per million per degree Celsius. Example: 22 ppm/C for alumminium.
237  # @return freqShiftRight = Relative frequency shift of the mask to the right = alpha*(Tmax-Tamb)*1e-6.
238  # @return freqShiftLeft = Relative frequency shift of the mask to the left = alpha*(Tamb-Tmin)*1e-6.
239  #
240  def getTempVar():
241  temVarDlg = TempVarDlg(parent)
242  if temVarDlg.exec_():
243  if temVarDlg.temVar_groupBox.isChecked() == True:
244  alpha = temVarDlg.coef_doubleSpinBox.value()*1e-6
245  Tamb = temVarDlg.Tamb_doubleSpinBox.value()
246  Tmax = temVarDlg.Tmax_doubleSpinBox.value()
247  Tmin = temVarDlg.Tmin_doubleSpinBox.value()
248  return alpha*(Tmax-Tamb), alpha*(Tamb-Tmin)
249  else:
250  return 0, 0
251  else:
252  return None, None
253 
254 
255  ##
256  #
257  # Helper function that returns next item that does not start with '!' of iterLines iterator.
258  # Items returned by iterLines iterator must be strings.
259  # @return line = First item that does not start with '!'
260  #
261  def nextNonComment():
262  line = file.readline()
263  while len(line)>0 and (line.startswith('!') or len(line.strip()) == 0): line = file.readline()
264  return line
265 
266  ##
267  #
268  # Helper function that returns the conversion factor corresponding to 'units' argument.
269  # @param units = Units. Must be a key in the unitsFactorDict dictionary. String.
270  # @return factor = Frequency conversion factor
271  #
272  def getUnitsFactor(units):
273  try: factor = unitsFactorDict[units]
274  except KeyError, err: raise parseError, "Incorrect frequency units '%s', should be one of: %s" % (units, str(unitsFactorDict.keys()))
275  return factor
276 
277  ##
278  #
279  # Helper function that returns the data read form specification mask file.
280  # The units conversion factors are applied.
281  # @param line = String to process (line of specification mask file).
282  # @return data = Numpy array containing the data.
283  # @return line = String containing next non-comment line of specification mask file after the data, normally starting with '#'.
284  #
285  def getData(line):
286  # Get unit factors
287  unitsFactors = np.array([ getUnitsFactor(units) for units in unitsList ] )
288 
289  data = []
290  while len(line)>0 and not line.startswith('#'): # Check len(line)>0 to detect EOF
291 
292  # Get data and check dimensions
293  xArray = np.array( [ float(x) for x in line.split() ] )
294  if len(xArray) != len(unitsFactors): raise parseError, "Number of data columns different than number of units in Mask format line (%s)" % str([parType] + unitsList)
295  data.append(xArray * unitsFactors)
296 
297  #Get next line
298  line = nextNonComment()
299  data = np.array(data).squeeze()
300 
301  if data.ndim == 0: data = float(data) # An scalar
302 
303  return (data, line)
304 
305  # End helper functions
306 
307  fbasename = os.path.basename(fileName)
308 
309  ##
310  # File name
311  self.fileName = fbasename
312 
313  ##
314  # Parent window
315  self.mainWindow = parent
316 
317  ##
318  # Central frequency
319  self.f0 = None
320 
321  ##
322  # Bandwidth
323  self.BW = None
324 
325  ##
326  # Specification for S11 (return losses) in dB. Float.
327  self.S11spec = None
328 
329  ##
330  # Value of S21 at f0
331  self.S21_F0 = None
332 
333  ##
334  # Value of S21 over bandwidth
335  self.S21_BW = None
336 
337  ##
338  # Specification for S21 variation over bandwidth (insertion losses) in dB pk-pk.
339  # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
340  self.S21inbandSpecDelta = None
341 
342  ##
343  # Specification for S21 out of band rejection in dB.
344  # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
345  self.S21outbandSpec = None
346 
347  ##
348  # Group delay mask (sec).
349  # Numpy float array Nx2 where N is the number of points, frequency data (Hz) is in the 1st column and Mask data in the 2nd.
350  self.GDspec = None
351 
352  ##
353  # Frequency samples in the pass band for optimization
354  self.freqInbandOptim = None
355 
356  ##
357  # Frequency samples in the rejection band for optimization
358  self.freqOutBandOptim = None
359 
360  ##
361  # Array with all frequency samples for optimization, in Hz, the first self.NfreqInBandOptim correspond to self.freqInbandOptim and the rest ot self.freqOutBandOptim
362  self.evalFoptim = None
363 
364  ##
365  # Array with all frequency samples for optimization, in normalized s-plane, the first self.NfreqInBandOptim correspond to self.freqInbandOptim and the rest ot self.freqOutBandOptim
366  self.evalSoptim = None
367 
368  ##
369  # Number of samples in self.evalSoptim corresponding to the pass band = len(self.freqInbandOptim)
370  self.NfreqInBandOptim = None
371 
372  ##
373  # S11 mask samples in the pass band for optimization
374  self.S11optim = None
375 
376  ##
377  # S21 mask samples in the pass band for optimization
378  self.S21inBandOptim = None
379 
380  ##
381  # S21 mask samples in the rejection band for optimization
382  self.S21outBandOptim = None
383 
384 
385  try:
386  file = open(fileName, 'r')
387  except IOError, err:
388  criticalErrorMsg( 'Cannot open Mask specification file:<p>' + unicode(err) )
389  myPrint('Failed to open Mask specification file: %s' % fbasename)
390  return
391 
392  freqShiftRight, freqShiftLeft = getTempVar()
393  if freqShiftLeft is None and freqShiftRight is None:
394  freqShiftLeft, freqShiftRight = 0, 0 # Cancelling or closing the dialog is equal to no temperature variation
395  myPrint('Mask temperature freq shift (relative) = %.2e to the right and %.2e to the left' % (freqShiftRight, freqShiftLeft))
396 
397  ##
398  # Factor to multiply the frequencies that shift to the right
399  self.freqFactorRight = 1+freqShiftRight
400 
401  ##
402  # Factor to multiply the frequencies that shift to the left
403  self.freqFactorLeft = 1-freqShiftLeft
404 
405  myPrint('Reading [S] Mask specification file: %s' % fbasename)
406 
407  unitsFactorDict = { 'Hz': 1, 'kHz': 1e3, 'MHz': 1e6, 'GHz': 1e9, 'dB': 1, 's': 1, 'ms': 1e-3, 'us': 1e-6, 'ns': 1e-9 }
408 
409  try:
410  line = nextNonComment()
411  while len(line) > 0: # while not EOF
412  if not line.lstrip().startswith('#'): raise parseError, 'Incorrect file format'
413  optionList = line[1:].split() # Leading '#' removed
414  parType, unitsList = optionList[0], optionList[1:]
415  line = nextNonComment()
416 
417  if parType.upper() == 'f0'.upper(): self.f0, line = getData(line)
418  elif parType.upper() == 'BW'.upper(): self.BW, line = getData(line)
419  elif parType.upper() == 'S11_BW'.upper(): self.S11spec, line = getData(line)
420  elif parType.upper() == 'S21_f0'.upper(): self.S21_F0, line = getData(line)
421  elif parType.upper() == 'S21_BW'.upper(): self.S21_BW, line = getData(line)
422  elif parType.upper() == 'S21_f_inband'.upper(): self.S21inbandSpecDelta, line = getData(line)
423  elif parType.upper() == 'GD_f'.upper(): self.GDspec, line = getData(line)
424  elif parType.upper() == 'S21_f_outband'.upper(): self.S21outbandSpec, line = getData(line)
425  file.close()
426 
427  except (parseError, ValueError), err:
428  criticalErrorMsg( 'ERROR parsing specifications mask file:<p>' + unicode(err) )
429  myPrint('Failed to read file: %s' % fbasename)
430  else:
431  myPrint('Loaded file: %s' % fbasename)
432 
433 
434  ##
435  #
436  # Process specification data to obtain the mask curves.
437  # The mask values are computed from specification relative values adding the relevant [S] parameter data.
438  # @param SP = Sparameters class instance
439  #
440  def specToMask(self, SP):
441 
442  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!
443  # BEGIN: Helper function
444 
445  ##
446  #
447  # Apply frequency shift to mask.
448  # @param mask = Mask numpy array
449  # @param type = 'outer' or 'inner'.
450  # 'outer' masks expand: the right freq shift is applied to the left portion of the data and the left frequency shift to the right portion.
451  # 'inner' masks collapse: the left freq shift is applied to the left portion of the data and the right frequency shift to the right portion.
452  #
453  def applyFreqShift(mask, type):
454  if mask is not None:
455  M = mask.shape[0]
456 
457  assert type in ['outer', 'inner']
458  if type == 'outer':
459  (leftPartfactor, rightPartFactor) = (self.freqFactorRight, self.freqFactorLeft)
460  elif type == 'inner':
461  (leftPartfactor, rightPartFactor) = (self.freqFactorLeft, self.freqFactorRight)
462 
463  # Apply factors. Works for M even or odd using integer division (5/2=2).
464  # Use int() just in case future versions of python implement floating point division.
465  mask[:int(M/2), 0] *= leftPartfactor
466  mask[int((M+1)/2):, 0] *= rightPartFactor
467 
468  # END: Helper function
469  #!!!!!!!!!!!!!!!!!!!!!!!!!!!!
470 
471  ##
472  # Mask for S11 (return losses) as a function of frequency. Numpy array.
473  self.S11mask = None
474 
475  if self.S11spec is not None:
476  self.S11mask = np.array([ (self.f0 - self.BW/2, -self.S11spec), (self.f0 + self.BW/2, -self.S11spec) ])
477 
478  ##
479  # Mask for S21 inband (insertion losses)
480  self.S21inBandMask = None
481 
482  maxS21 = max( 20*np.log10(np.abs(SP.S21)) )
483 
484  if self.S21_BW is not None and self.S21_F0 is not None:
485  S21inband = np.array([ (self.f0 - self.BW/2, -self.S21_BW), (self.f0, -self.S21_F0), (self.f0 + self.BW/2, -self.S21_BW) ])
486  else: S21inband = np.array([[],[]]).T
487 
488  if self.S21inbandSpecDelta is not None:
489  S11variation = self.dataSymmetrizeDeltaF0(self.S21inbandSpecDelta, 0)
490  S11variation[:, 1] = maxS21 - S11variation[:, 1]
491  else: S11variation = np.array([[],[]]).T
492 
493  self.S21inBandMask = np.concatenate(( S21inband, S11variation ) )
494  if len(self.S21inBandMask) > 0: self.freqSort(self.S21inBandMask, 0)
495  else: self.S21inBandMask = None
496 
497  ##
498  # Mask for out-of-band rejection as a function of frequency. Numpy array.
499  self.S21outBandMask = None
500 
501  if self.S21outbandSpec is not None:
502  self.S21outBandMask = self.dataSymmetrizeDeltaF0(self.S21outbandSpec, 0, hideBW = True)
503  self.S21outBandMask[:, 1] = maxS21 - self.S21outBandMask[:, 1]
504 
505  ##
506  # Mask for group delay (upper).
507  self.GDmaskUpper = None
508 
509  ##
510  # Mask for group delay (lower).
511  self.GDmaskLower = None
512 
513  if self.GDspec is not None:
514  # Group delay at the centre frequency (average of GD between f0-BW/4 and f0+BW/4)
515  indBW = (SP.freq < self.f0 + self.BW/4 ) & (SP.freq > self.f0 - self.BW/4)
516  GDf0 = np.mean(SP.groupDelay[indBW])
517 
518  self.GDmaskUpper = self.dataSymmetrizeDeltaF0(self.GDspec, 0)
519  self.GDmaskUpper[:, 1] = GDf0 + self.GDmaskUpper[:, 1]
520 
521  self.GDmaskLower = self.dataSymmetrizeDeltaF0(self.GDspec, 0)
522  self.GDmaskLower[:, 1] = GDf0 - self.GDmaskLower[:, 1]
523 
524 
525  # Apply mask shift with temperature
526  for maskAndType in [ (self.S21outBandMask, 'outer'), (self.S11mask, 'inner'), (self.S21inBandMask, 'inner'), (self.GDmaskUpper, 'inner'), (self.GDmaskLower, 'inner') ]:
527  applyFreqShift(*maskAndType)
528 
529 # print self
530 
531 
532  ##
533  #
534  # Create mask samples to be used as a reference in optimization.
535  # Half the samples go to the passband and half to the rejection band (symmetrically).
536  # The frequency samples for optimization are obtained by linear interpolation of the mask data.
537  # Only the magnitude of [S] is optimized to comply with the S11 and S21 masks.
538  # Group delay in the mask file, if any, is not used in optimization,
539  #
540  # @param FT = FrequencyTransform class instance, containing f0 and BW parameters.
541  # @param Nsamples = Total number of samples. Half go to the passband and half to the rejection band (symmetrically).
542  #
543  def maskSamplesOptimization(self, FT, Nsamples):
544 
545  if self.S11mask is None:
546  raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S11_BW' optional parameter in the mask file."
547 
548  if self.S21outBandMask is None:
549  raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S21_f_outband' optional parameter in the mask file."
550 
551  if self.S21_F0 is None or self.S21_BW is None:
552  raise synthError, "For optimization with a reference mask, it is necessary to<br>define 'S21_F0' and 'S21_BW' optional parameters in the mask file."
553 
554  assert self.S21inBandMask is not None # Cannot be None if self.S21_F0 and self.S21_BW are not None
555 
556  # Epsilon to guarantee that optimization frequency samples are within the mask interval, with posible roundoff errorsº
557  eps = self.f0/1e12
558 
559  # Frequency sampling points in the pass band
560  NpassBand = int(Nsamples/2)
561  minFreqInBand = np.min( np.concatenate([ self.S11mask[:, 0], self.S21inBandMask[:, 0], self.GDmaskUpper[:, 0], self.GDmaskLower[:, 0] ]) ) + eps
562  maxFreqInBand = np.max( np.concatenate([ self.S11mask[:, 0], self.S21inBandMask[:, 0], self.GDmaskUpper[:, 0], self.GDmaskLower[:, 0] ]) ) - eps
563  self.freqInbandOptim = np.linspace(minFreqInBand, maxFreqInBand, NpassBand)
564 
565  # Frequency sampling points in the rejection band
566  # We have to remove the NaN samples from self.S21outBandMask,
567  NS21out = len(self.S21outBandMask)
568  assert NS21out % 2 == 0, 'Number of samples in S21outBandMask must be even' # Number of out band mask samples is always even, since the mask is symmetric
569  nonNaN = range(0, NS21out/2-1) + range(NS21out/2+1, NS21out) # Indices on no-NaN samples
570 
571  minFreqOutBandLower = np.min(self.S21outBandMask[:NS21out/2, 0]) + eps
572  maxFreqOutBandLower = np.max(self.S21outBandMask[:NS21out/2, 0]) - eps
573  minFreqOutBandUpper = np.min(self.S21outBandMask[1+NS21out/2:, 0]) +eps
574  maxFreqOutBandUpper = np.max(self.S21outBandMask[1+NS21out/2:, 0]) - eps
575 
576  NoutBand = int((Nsamples+1)/2)
577  freqOutBandLower = np.linspace(minFreqOutBandLower, maxFreqOutBandLower, int(NoutBand/2) )
578  freqOutBandUpper = np.linspace(minFreqOutBandUpper, maxFreqOutBandUpper, int((NoutBand+1)/2) )
579  self.freqOutBandOptim = np.concatenate([ freqOutBandLower, freqOutBandUpper ])
580 
581  # Convenience data to save computation in the cost function
582  self.evalFoptim = np.concatenate([ self.freqInbandOptim, self.freqOutBandOptim ]) # Necessary for BW optimization: the FT will change with BW
583  self.evalSoptim = 1j * FT.normFreq( self.evalFoptim )
584  self.NfreqInBandOptim = len(self.freqInbandOptim)
585 
586  # Interpolating functions
587  interpS11 = interpolate.interp1d(self.S11mask[:, 0], self.S11mask[:, 1], bounds_error = False)
588  interpS21inBand = interpolate.interp1d(self.S21inBandMask[:, 0], self.S21inBandMask[:, 1], bounds_error = False)
589  interpS21outBand = interpolate.interp1d(self.S21outBandMask[nonNaN, 0], self.S21outBandMask[nonNaN, 1], bounds_error = False)
590 
591  # Interpolated masks
592  self.S11optim = interpS11(self.freqInbandOptim)
593  self.S21inBandOptim = interpS21inBand(self.freqInbandOptim)
594  self.S21outBandOptim = interpS21outBand(self.freqOutBandOptim)
595 
596 
597  ##
598  #
599  # Sort and symmetrise data with respect to centre frequency self.f0 and add self.f0 to frequency column of array.
600  # Frequencies in the input array are delta with respect to centre frequency self.f0.
601  # Frequencies in the result are absolute.
602  # @param inArray = Input numpy array.
603  # @param freqCol = Column index of frequency values.
604  # @param hideBW = Flag to hide the centre of the mask. If True, the mask is not displayed at the centre, between the symmetric data points. Default False.
605  # @return outArray = Output numpy array
606  #
607  def dataSymmetrizeDeltaF0(self, inArray, freqCol, hideBW = False):
608  array1 = inArray.copy()
609  array2 = array1.copy()
610  array1[:, freqCol] = self.f0 - array1[:, freqCol]
611  array2[:, freqCol] = self.f0 + array2[:, freqCol]
612 
613  if hideBW:
614  closestFreq = min(inArray[:, 0]) # Minimum delta freq, which corresponds to the freq closest to f0
615  hideCenter = np.array([ [self.f0 - closestFreq, np.nan], [self.f0 + closestFreq, np.nan] ])
616  outArray = np.concatenate((array1, hideCenter, array2))
617  else:
618  outArray = np.concatenate((array1, array2))
619 
620  self.freqSort(outArray, freqCol)
621  return outArray
622 
623 
624  ##
625  #
626  # Sort array by frequency (inplace).
627  # The input array is modified inplace
628  # @param inArray = Input numpy array.
629  # @param freqCol = Column index of frequency values.
630  #
631  def freqSort(self, inArray, freqCol):
632  sortedIndices = inArray[:,freqCol].argsort()
633  inArray[:, :] = inArray[sortedIndices, :]
634 
635 
636  def __str__(self):
637  st = ''
638  st += '*** Specifications ***\n\n'
639  st += 'Central frequency:\n' + str(self.f0) + '\n\n'
640  st += 'Bandwidth:\n' + str(self.BW) + '\n\n'
641  st += 'Specification for S11 (return losses):\n' + str(self.S11spec) + '\n\n'
642  st += 'Value of S21 at f0 (insertion losses):\n' + str(self.S21_F0) + '\n\n'
643  st += 'Value of S21 over bandwidth (insertion losses):\n' + str(self.S21_BW) + '\n\n'
644  st += 'S21 variation over bandwidth (insertion losses):\n' + str(self.S21inbandSpecDelta) + '\n\n'
645  st += 'S21 out of band rejection: \n' + str(self.S21outbandSpec) + '\n\n'
646  st += 'Group delay mask:\n' + str(self.GDspec) + '\n\n'
647 
648  st += '*** Masks ***\n\n'
649  st += 'Mask for S11 (return losses): \n' + str(self.S11mask) + '\n\n'
650  st += 'Mask for S21 in-band (insertion losses)\n' + str(self.S21inBandMask) + '\n\n'
651  st += 'Mask for S21 for out-of-band rejection: \n' + str(self.S21outBandMask) + '\n\n'
652  st += 'Mask for GD (upper): \n' + str(self.GDmaskUpper) + '\n\n'
653  st += 'Mask for GD (lower): \n' + str(self.GDmaskLower) + '\n\n'
654 
655  return st
656 
657 
658 ##
659 #
660 # [S] parameters class, as read from Touchstone file.
661 # It contains the minimum number of attributes to plot the [S] parameters in the results comparison window.
662 # Of course, the attributes share the same name as the corresponding ones in libcommonfunc.Sparameters class.
663 #
664 class Stouchstone(object):
665 
666  ##
667  #
668  # Set frequency axis and S parameters, including phase and group delay, read from a Touchstone file.
669  # Touchstone is a trademark of Agilent Corporation
670  # @param fileName = File Name
671  #
672  def __init__(self, fileName):
673 
674  fbasename = os.path.basename(fileName)
675 
676  try:
677  file = open(fileName, 'r')
678  except IOError, err:
679  criticalErrorMsg( 'Cannot open [S] Touchstone file:<p>' + unicode(err) )
680  myPrint('Failed to open [S] Touchstone file: %s' % fbasename)
681  return
682 
683  myPrint('Reading [S] from Touchstone file: %s' % fbasename)
684 
685  freqFactor = 1e9
686  dataFormat = 'MA'
687  freqfactorDict = { 'HZ': 1, 'KHZ': 1e3, 'MHZ': 1e6, 'GHZ': 1e9}
688  optionsLoaded = False
689  freq, S11, S21, S12, S22 = [], [], [], [], []
690  Sread = False
691 
692  try:
693  stList = file.readlines()
694  for st in stList:
695  if len(st.lstrip()) == 0: continue # Empty line
696  if st.lstrip().startswith('!'):
697  if Sread: break # If S parameters have been already read, this comment line will start noise specifications, and we want to quit here.
698  else: continue # A comment
699 
700  if st.lstrip().startswith('#'): # The options string
701  if optionsLoaded: raise parseError, "There are more than one options line."
702 
703  optionList = st.split()
704 
705  # Check number of options
706  if len(optionList) != 6: raise parseError, "Incorrect number of items in option line."
707 
708  # Parse frequency units
709  units = optionList[1].upper()
710  try: freqFactor = freqfactorDict[units]
711  except KeyError, err: raise parseError, "Incorrect frequency units '%s'" % units
712 
713  # Parse type of parameter
714  type = optionList[2]
715  if type != 'S': raise parseError, "Type of parameter is not 'S'."
716 
717  # Parse data format
718  dataFormat = optionList[3].upper()
719  if dataFormat not in ['MA', 'DB', 'RI']: raise parseError, "Data format '%s' is not 'MA', 'DB', or 'RI'." % dataFormat
720 
721  # Parse reference impedance
722  if optionList[4] != 'R': raise parseError, "Options line does not include 'R' parameter."
723  impedance = optionList[5]
724 
725  optionsLoaded = True
726 
727  else:
728  try: f, S11a, S11b, S21a, S21b, S12a, S12b, S22a, S22b = [ float(n) for n in st.split()]
729  except ValueError: raise parseError, "Data line '%s' contains incorrect number of items" % st
730 
731  freq.append(f * freqFactor)
732 
733  if dataFormat == 'RI':
734  S11.append( complex(S11a, S11b) )
735  S21.append( complex(S21a, S21b) )
736  S12.append( complex(S12a, S12b) )
737  S22.append( complex(S22a, S22b) )
738  if dataFormat == 'MA':
739  S11.append( S11a * np.exp(1j * S11b * pi/180) )
740  S21.append( S21a * np.exp(1j * S21b * pi/180) )
741  S12.append( S12a * np.exp(1j * S12b * pi/180) )
742  S22.append( S22a * np.exp(1j * S22b * pi/180) )
743  if dataFormat == 'DB':
744  S11.append( 10**(S11a/20) * np.exp(1j * S11b * pi/180) )
745  S21.append( 10**(S21a/20) * np.exp(1j * S21b * pi/180) )
746  S12.append( 10**(S12a/20) * np.exp(1j * S12b * pi/180) )
747  S22.append( 10**(S22a/20) * np.exp(1j * S22b * pi/180) )
748  Sread = True
749 
750  file.close()
751 
752  except parseError, err:
753  criticalErrorMsg( 'ERROR parsing [S] Touchstone file:<p>' + unicode(err) )
754  myPrint('Failed to read file: %s' % fbasename)
755  else:
756  myPrint('Loaded file: %s' % fbasename)
757 
758  ##
759  # Frequency axis (Hz)
760  self.freq = np.array(freq)
761 
762  ##
763  # Parameter \f$ S_{11} \f$
764  self.S11 = np.array(S11)
765 
766  ##
767  # Parameter \f$ S_{12} \f$
768  self.S21 = np.array(S21)
769 
770  ##
771  # Parameter \f$ S_{21} \f$
772  self.S12 = np.array(S12)
773 
774  ##
775  # Parameter \f$ S_{22} \f$
776  self.S22 = np.array(S22)
777 
778  ##
779  # Phase of return losses (rad)
780  self.phaseS11 = np.unwrap(np.angle(self.S11))
781 
782  ##
783  # Phase of inverse transfer function (rad)
784  self.phaseS12 = np.unwrap(np.angle(self.S12))
785 
786  ##
787  # Phase of transfer function (rad)
788  self.phaseS21 = np.unwrap(np.angle(self.S21))
789 
790  ##
791  # Phase of inverse return losses (rad)
792  self.phaseS22 = np.unwrap(np.angle(self.S22))
793 
794  ##
795  # Frequency axis with sampling points in the middle of self.freq sampling.
796  # It is necessary to plot self.groupDelay2
797  self.freq2 = (self.freq[:-1] + self.freq[1:])/2
798 
799  ##
800  # Group delay for the unnormalized filter (sec), sampled at self.freq2
801  self.groupDelay2 = -np.diff(self.phaseS21) / (2*pi* np.diff(self.freq))
802 
803  # Set causality principle. There may be spurious negative peaks due to computing phase at transmission zeros.
804  self.groupDelay2[ self.groupDelay2<0 ] = 0
805 
806 
807 ##
808 #
809 # Subclass for:
810 # - reimplementation of SinzeHint and MinimumSizeHint
811 # - Methods for tables of transmission zeros
812 #
813 class myTableWidget(QTableWidget):
814 
815  def __init__(self, name = None, mainDlg = None, parent=None):
816  super(myTableWidget, self).__init__(parent)
817 
818  ##
819  # Table name.
820  self.name = name
821 
822  ##
823  # Main dialog to which this table belongs, used only where applicable.
824  self.mainDlg = mainDlg
825 
826  ##
827  # Frequency units, used only where applicable.
828  self.units = None
829 
830  ##
831  # Filter center frequency.
832  # It will be used for computation of 'f-f0' column and for autoupdate of 'Imag' and 'f-f0' colums when one of them is edited.
833  self.f0 = None
834 
835  ##
836  # Flag to inform that changes in table cells are due to user interaction
837  self.updatedByUser = True
838 
839 
840  ##
841  # Store units in self.untis, to use in TZ table headers.
842  # @param units = Units string.
843  #
844  def setUnits(self, units):
845  self.units = units
846 
847 
848  ##
849  #
850  # Set the value in Center Frequency widget as self.f0.
851  # It will be used for computation of 'f-f0' column and for autoupdate of 'Imag' and 'f-f0' colums when one of them is edited.
852  # @param updateImag: If updateImag is True, then the 'Imag' column of the whole table is updated according to f0 and to the f-f0 column.
853  # If updateImag is False, f0 is only stored in self.f0 and the 'Imag' column is not updated.
854  #
855  def setf0(self, updateImag = True):
856  self.f0 = float(self.mainDlg.f0_lineEdit.text()) * 10**(3*self.mainDlg.f0UnitsIndex)
857 
858  if updateImag: # Update TZ Imag column according to f0 and values of f-f0 column
859 
860  M = self.rowCount()
861  freqFactor = pow(10, 3*self.mainDlg.transmissionZerosUnitsIndex)
862 
863  for m in range(M):
864  item = self.item(m, 2)
865  stIm = str(item.text()).strip() if item is not None else ''
866 
867  if stIm == '':
868  newItem = QTableWidgetItem('')
869  self.setItem(m, 1, newItem)
870  else:
871  im = float(stIm) * freqFactor
872  newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im + self.f0) / freqFactor) )
873  newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
874  self.setItem(m, 1, newItem)
875 
876 
877  ##
878  #
879  # Resize rows and columns to fit contents
880  #
881  def updateTableSize(self):
882  self.resizeRowsToContents()
883  self.resizeColumnsToContents()
884  self.setMaximumSize( self.maximumSizeHint() )
885 
886 
887  ##
888  #
889  # Table cell has been modified by user, take necessary actions.
890  # If Imag or Imag-f0 columns have been modified, updated the companion cell
891  #
892  def tableCellChanged(self, row, col):
893 
894  if not self.updatedByUser: return # Do nothing if it changes are not due to user
895 
896  # Avoid infinite recursion
897  self.updatedByUser = False
898 
899  freqFactor = pow(10, 3*self.mainDlg.transmissionZerosUnitsIndex)
900 
901  self.setCurrentCell(row, col)
902  item = self.currentItem()
903  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter) # It may be an item automatically created with default alignment
904  # Here item string is a valid float or an empty string '', since it has been filtered before
905  stIm = str(item.text()).strip() if item is not None else ''
906 
907  if stIm == '': # If updated cell has nothing, put also nothing in the companion cell
908  newItem = QTableWidgetItem('')
909  if col == 1: self.setItem(row, 2, newItem)
910  elif col == 2: self.setItem(row, 1, newItem)
911  else:
912  im = float(stIm) * freqFactor
913  if col == 1: # Imag has been changed
914  newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im - self.f0) / freqFactor) )
915  newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
916  self.setItem(row, 2, newItem)
917  elif col == 2: # f-f0 has been changed
918  newItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (im + self.f0) / freqFactor) )
919  newItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
920  self.setItem(row, 1, newItem)
921 
922  self.updateTableSize()
923  self.mainDlg.updateTotaltransmissionZeros()
924 
925  self.updatedByUser = True
926 
927 
928  ##
929  #
930  # Append new row for a new zero
931  #
932  def addRow(self):
933 
934  N = self.rowCount()
935  self.setRowCount(N+1)
936 
937  self.setCurrentCell(N, 1)
938  self.resizeRowsToContents()
939  self.resizeColumnsToContents()
940  self.setMaximumSize( self.maximumSizeHint() )
941  self.mainDlg.updateDlgHeight()
942 
943 
944  def sizeHint(self):
945  return maximumSizeHint()
946 
947 
948  def maximumSizeHint(self):
949  #size = super(myTableWidget, self).sizeHint()
950  size = QTableWidget.sizeHint(self)
951 
952  # Compute space for table with headers and ScrollBars (just in case they appear).
953  # At this point, the size of the ScrollBar is given by self.horizontalScrollBar().height(),
954  # for both the horizontal and vertical ScrollBars.
955  # self.verticalScrollBar().width() contains and absurd vale here.
956  width = self.verticalHeader().width() + self.horizontalScrollBar().height()
957  for c in range(self.columnCount()): width += self.columnWidth( c )
958  size.setWidth( width + 8 )
959 
960  height = self.horizontalHeader().height() + self.horizontalScrollBar().height()
961  for r in range(self.rowCount()): height += self.rowHeight( r )
962  size.setHeight( height + 4 )
963 
964 # if self.name is not None: print self.name
965 # print 'HSB:', self.horizontalScrollBar().isVisible(), self.horizontalScrollBar().width(), self.horizontalScrollBar().height()
966 # print 'VSB:', self.verticalScrollBar().isVisible(), self.verticalScrollBar().width(), self.verticalScrollBar().height()
967 
968  return size
969 
970 ##
971 #@todo Reimplment myTableWidget.resizeEvent() function to reduce size of table when there are no ScrollBars
972 
973 # def resizeEvent(self, event):
974 # ''' Reimplementation of the resize event to check state of the toolbars.
975 # '''
976 # if self.horizontalScrollBar().isVisible() or self.verticalScrollBar().isVisible():
977 # maxSize = self.maximumSizeHint()
978 # if self.horizontalScrollBar().isVisible(): maxSize.setHeight(maxSize.height() - self.horizontalScrollBar().height())
979 # if self.verticalScrollBar().isVisible(): maxSize.setWidth(maxSize.height() - self.horizontalScrollBar().height())
980 # self.setMaximumSize(maxSize)
981 #
982 # if self.name is not None: print self.name
983 # print 'event new size:' , event.size().width(), event.size().height()
984 # print 'event old size:' , event.oldSize().width(), event.oldSize().height()
985 # print 'self:', self.width(), self.height()
986 # print 'HSB:', self.horizontalScrollBar().isVisible(), self.horizontalScrollBar().width(), self.horizontalScrollBar().height()
987 # print 'VSB:', self.verticalScrollBar().isVisible(), self.verticalScrollBar().width(), self.verticalScrollBar().height()
988 
989 
990  def sizeHint(self):
991  return self.maximumSizeHint()
992 
993 
994 ##
995 #
996 # GUI main window class.
997 #
998 class MainWindow(QMainWindow, Ui_mainwindow.Ui_MainWindow):
999 
1000  ##
1001  #
1002  # Constructor to initialize main window.
1003  # @param fileName = Name of file to automatically open after creating the window. If equal to None, no file is open.
1004  # @param parent = Parent widget, in this case is None.
1005  #
1006  def __init__(self, fileName, parent=None):
1007  super(MainWindow, self).__init__(parent)
1008  self.setupUi(self)
1009 
1010  ##
1011  # Variable to store the modified or unmodifed status of parameters class instance P.
1012  self.dirty = False
1013 
1014  ##
1015  # Variable that will avoid cleanup if set to True.
1016  # It is used when a method that calls self.clenaup() is called by the GUI and not by the user.
1017  # The user may want to avoid cleanup in some situations.
1018  self.noCleanup = False
1019 
1020  ##
1021  # Name of the parameters file to process.
1022  # When main Window is created, self.fileName is a copy of the file name given in the command line when calling the application.
1023  # Later self.fileName can be modified when opening a parameter file with the GUI.
1024  self.fileName = fileName
1025 
1026  ##
1027  # Application name, to be accesible outside this module.
1028  self.applicationName = applicationName
1029 
1030  self.settings = QSettings()
1031  self.restoreGeometry(self.settings.value("Geometry").toByteArray())
1032  self.restoreState(self.settings.value("State").toByteArray())
1033 
1034  ##
1035  # Last open directory name
1036  self.lastDir = unicode(self.settings.value('LastDirectoryOpened', QVariant('.')).toString())
1037 
1038  # Set window title , initially there is no modified file to save
1039  self.setWindowTitle(applicationName)
1040  self.setWindowModified(False)
1041 
1042  # Disable selections in log_listWidget
1043  self.log_listWidget.setSelectionMode(QAbstractItemView.NoSelection)
1044 
1045  # Add results selection comboBox to toolBar
1046  self.results_comboBox = QComboBox(self.toolBar_2)
1047  self.results_comboBox.setObjectName("results_comboBox")
1048  self.results_comboBox.setToolTip("Load result from list")
1049  self.results_comboBox.setStatusTip("Load result from list")
1050  self.results_comboBox.setWhatsThis("Load result from list")
1051  self.connect(self.results_comboBox, SIGNAL("currentIndexChanged(int)"), self.on_results_comboBox_currentIndexChanged)
1052  self.toolBar_2.insertWidget(self.action_ListRemove, self.results_comboBox)
1053 
1054  # Add what's this action
1055  self.toolBar_3.insertAction(self.action_FileExit, QWhatsThis.createAction(self.toolBar_3))
1056 
1057  ##
1058  # Parameters class instance containing filter and synthesis parameters.
1059  # Read from the parameters file and -maybe- modified with the Parameter Edit Dialog.
1060  self.P = None
1061 
1062  ##
1063  # Copy of the CharPolys class instance containing characteristic polynomials.
1064  self.CP = None
1065 
1066  ##
1067  # Copy of the CouplingMatrices class instance containing coupling matrices.
1068  self.CM = None
1069 
1070  ##
1071  # Copy of the SParameters class instance containing [S] parameters.
1072  self.SP = None
1073 
1074  ##
1075  # Copy of the FrequencyTransform class instance containing the frequency transform.
1076  self.FT = None
1077 
1078  ##
1079  # Specification Mask class instance.
1080  self.SM = None
1081 
1082  ##
1083  # Flag that is True when a successful computation has been done.
1084  # When the flag is False, no results are plotted.
1085  self.computed = False
1086 
1087  ##
1088  # QMainWindow that contains the parameter edit window.
1089  self.paramEdit = None
1090 
1091  ##
1092  # QwtPlot that contains the Magnitude and Phase S-parameter plot.
1093  self.SPlotMagPhase = None
1094 
1095  ##
1096  # QwtPlot that compares S-parameters obtained from CP from those obtained from CM.
1097  self.SPlotError = None
1098 
1099  ##
1100  # QwtPlot that compares S-parameters for different results.
1101  self.SPlotCompare = None
1102 
1103  ##
1104  # QwtPlot that contains stored energy and dissipated power
1105  self.EnergyPlot = None
1106 
1107  ##
1108  # QwtPlot that contains S-parameters for sensitivity analysis
1109  self.SPlotSensitivity = None
1110 
1111  ##
1112  # QMainWindow that contains the coupling matrix table.
1113  self.CMdialog = None
1114 
1115  ##
1116  # List of results to compare
1117  self.listResults = [ ]
1118 
1119  ##
1120  # Last index of results selection comboBox
1121  self.indexResults = None
1122 
1123  ##
1124  # Flag that controls if frequency axis must be autoscaled. It will become true after changing the sweep min and max frequencies.
1125  self.autoScaleFreq = False
1126 
1127  ##
1128  # Store the mainWindow class instance into global variable 'mainWindow' defined in the libcommonfunc.py and libfreefilters.py
1129  # in order to let the console interface functions in libcommonfunc.py and libfreefilters.py access the GUI mainWindow.
1130  # The GUI methods access the mainWindow class through ParamEditDlg.mainWindow and MainWindow self attributes.
1131  setGlobalVariablesLibCommonFunc(mainWindow = self)
1132  setGlobalVariablesLibFreeFilters(mainWindow = self)
1133 
1134  # Print startup output in log window. Must import here the preGuiOutput variable, to catch its current value.
1135  from libcommonfunc import preGuiOutput
1136  myPrint(preGuiOutput)
1137 
1138  # Read file if there is one to read
1139  if self.fileName is not None:
1140  self.fileRead(self.fileName)
1141  if autoRun:
1142  self.action_Execute.trigger()
1143  if autoMatrix:
1144  self.action_CouplingMatrix.trigger()
1145  if autoEditMatrix:
1146  self.CMdialog.action_Edit.setChecked(True)
1147 
1148 
1149  ##
1150  #
1151  # This function checks if there are unsaved changes and, if there are, aks the user to save them.
1152  # The function is executed by the GUI when there is an action that will destroy the parameter class P: Close application, Open file, New file, etc.
1153  # @return ok (True / False) = If True the use has agreed to continue, if False the user cancels the operation.
1154  #
1155  def okToContinue(self):
1156  if self.dirty:
1157  if self.fileName is not None:
1158  message = "File %s has unsaved changes\nSave?" % os.path.basename(self.fileName)
1159  else:
1160  message = "New file with unsaved changes\nSave?"
1161  reply = QMessageBox.question(self, "%s - Unsaved Changes" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
1162  if reply == QMessageBox.Cancel:
1163  return False
1164  elif reply == QMessageBox.Yes:
1165  self.on_action_FileSaveAs_triggered(False)
1166  return True
1167 
1168 
1169  ##
1170  #
1171  # Print message and update window title.
1172  # Prints message in the log widget at the main window background and also at the status bar for 5 sec.
1173  # Updates main window title with open file name and modified state.
1174  # @param message = Message to print (string).
1175  #
1176  def updateStatus(self, message):
1177  self.statusBar().showMessage(message, 5000)
1178  self.log_listWidget.addItem(message)
1179  self.log_listWidget.setCurrentRow(self.log_listWidget.count()-1)
1180  if self.fileName is not None:
1181  self.setWindowTitle("%s - %s[*]" % (applicationName , os.path.basename(self.fileName)) )
1182  elif self.P is not None and self.P.empty is not True:
1183  self.setWindowTitle("%s - Unnamed[*]" % applicationName)
1184  else:
1185  self.setWindowTitle("%s[*]" % applicationName)
1186  self.setWindowModified(self.dirty)
1187 
1188 
1189  ##
1190  #
1191  # Reads parameters from a file.
1192  # @param fname = File name.
1193  #
1194  def fileRead(self, fname):
1195  # Initally set filename to None just in case the file parsing fails and the file is not read.
1196  self.fileName = None
1197  self.P = readParamFile(fname)
1198  fbasename = os.path.basename(fname)
1199  # Update self.lastDir
1200  self.lastDir = os.path.dirname(fname)
1201 
1202  if self.P is None:
1203  QMessageBox.warning(self, "WARNING",
1204  """<p align="center">It is not possible to open file %s<br>
1205  because it contains an invalid entry:</p>
1206  <p align="center">Please open the file with a text editor and fix it.
1207  </p>
1208  """ % fbasename )
1209  message = "Failed to read file: %s" % fbasename
1210  else:
1211  self.fileName = fname
1212  message = "Loaded file: %s" % fbasename
1213  self.updateStatus(message)
1214 
1215  # Create Frequency Transform class instance for this parameters
1216  if self.P is not None:
1217  self.FT = FrequencyTransformBP(self.P)
1218 
1219  # Open parameter editor dialog
1220  if self.P is not None:
1221  self.paramEdit = ParamEditDlg(self)
1222  self.paramEdit.show()
1223  self.paramEdit.raise_()
1224  #self.paramEdit.activateWindow()
1225 
1226 
1227  ##
1228  #
1229  # Close SPlotMagPhase plot window, if it exists.
1230  #
1232  # Close CouplingMatrix dialog if it exists
1233  if self.SPlotMagPhase is not None and not self.SPlotMagPhase.closed:
1234  self.SPlotMagPhase.close()
1235  self.SPlotMagPhase = None
1236 
1237 
1238  ##
1239  #
1240  # Close SPlotCompare plot windows, if they exist.
1241  #
1243  if self.SPlotCompare is not None and not self.SPlotCompare.closed:
1244  self.SPlotCompare.close()
1245  self.SPlotCompare = None
1246 
1247 
1248  ##
1249  #
1250  # Close Coupling Matrix window and CM error plot window, if they exist.
1251  #
1252  def closeCMWindows(self):
1253  # Close CouplingMatrix dialog if it exists
1254  if self.CMdialog is not None:
1255  self.CMdialog.close()
1256  self.CMdialog = None
1257 
1258  # Close [S] error plot if it exists
1259  if self.SPlotError is not None and not self.SPlotError.closed:
1260  self.SPlotError.close()
1261  self.SPlotError = None
1262 
1263  # Close energy plot if it exists
1264  if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1265  self.EnergyPlot.close()
1266  self.EnergyPlot = None
1267 
1268 
1269  ##
1270  #
1271  # Clean up previous results and any open plot or coupling matrix window.
1272  # Clean up is disabled if self.nocleanup is True, in that case this method does nothing.
1273  # Sets self.dirty to False.
1274  #
1275  def cleanup(self):
1276  if self.noCleanup is True: return
1277 
1279  self.closeCMWindows()
1280  (self.P, self.CP, self.CM, self.SP, self.FT) = (None, None, None, None, None)
1281 
1282 
1283  ##
1284  #
1285  # Plot [S] parameters magnitude and phase graph, if they are available.
1286  #
1287  def plotSParameters(self):
1288 
1289  if self.SP is not None:
1290  if self.SPlotMagPhase is None or self.SPlotMagPhase.closed is True:
1291 
1292  # Set dynamic range to the lowest value of S21 at sweep range ends.
1293  linRange = min([ abs(self.SP.S21[0]), abs(self.SP.S21[-1] )])
1294  if linRange > 1e-10: dynamicRange = -20*log10(linRange)
1295  else: dynamicRange = 200
1296  dynamicRange = 10*ceil(dynamicRange/10)
1297 
1298  self.SPlotMagPhase = DbPlot( parent=self,
1299  windowTitle = '[S] parameters',
1300  tabTitle = '[S] + Group delay',
1301  title = '[S] parameters (from Characteristic Polynomials)',
1302  XData = self.SP.freq / 1e9,
1303  leftYData = [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1304  rightYData = [ 1e9 * self.SP.groupDelay ],
1305  leftYmin = -dynamicRange,
1306  leftYmax = 0,
1307  leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
1308  leftYVisible = [True, True, False],
1309  XLabel = 'Frequency (GHz)',
1310  leftYLabel = 'dB',
1311  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'ns',
1312  rightYLabel = 'Time (ns.)' ,
1313  rightYNames = ['Group delay'],
1314  rightYVisible = [True]
1315  )
1316  self.SPlotMagPhase.addTab( tabTitle = '[S] + Phase',
1317  title = '[S] parameters (from Characteristic Polynomials)',
1318  XData = self.SP.freq / 1e9,
1319  leftYData = [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22)) ],
1320  rightYData = [ self.SP.phaseS21 * 180/np.pi, self.SP.phaseS11 * 180/np.pi, self.SP.phaseS22 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1321  leftYmin = -dynamicRange,
1322  leftYmax = 0,
1323  leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
1324  leftYVisible = [True, True, False],
1325  XLabel = 'Frequency (GHz)',
1326  leftYLabel = 'dB',
1327  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg',
1328  rightYLabel = 'degrees' ,
1329  rightYNames = ['Phase(S<sub>21</sub>)', 'Phase(S<sub>11</sub>)', 'Phase(S<sub>22</sub>)', 'Deviation S<sub>21</sub> from <br>linear phase'],
1330  rightYVisible = [True, False, False, False]
1331  )
1332  self.SPlotMagPhase.addTab( tabTitle = 'Group delay + Phase',
1333  title = '[S] parameters (from Characteristic Polynomials)',
1334  XData = self.SP.freq / 1e9,
1335  leftYData = [1e9 * self.SP.groupDelay],
1336  rightYData = [ self.SP.phaseS21 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1337  leftYNames = ['Group delay'],
1338  XLabel = 'Frequency (GHz)',
1339  leftYLabel = 'Time (ns.)',
1340  Xunits = 'GHz', leftYunits = 'ns', rightYunits = 'deg',
1341  rightYLabel = 'degrees' ,
1342  rightYNames = ['Phase(S<sub>21</sub>)', 'Deviation from <br>linear phase'],
1343  rightYVisible = [True, False]
1344  )
1345 
1346  else:
1347  self.SPlotMagPhase.update(0, self.SP.freq / 1e9, [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1348  rightYData=[ 1e9 * self.SP.groupDelay] , autoScaleBottomX = self.autoScaleFreq)
1349 
1350  self.SPlotMagPhase.update(1, self.SP.freq / 1e9, [20*np.log10(np.abs(self.SP.S11)), 20*np.log10(np.abs(self.SP.S21)), 20*np.log10(np.abs(self.SP.S22))],
1351  rightYData=[ self.SP.phaseS21 * 180/np.pi, self.SP.phaseS11 * 180/np.pi, self.SP.phaseS22 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1352  autoScaleBottomX = self.autoScaleFreq )
1353 
1354  self.SPlotMagPhase.update(2, self.SP.freq / 1e9, [1e9 * self.SP.groupDelay], rightYData=[ self.SP.phaseS21 * 180/np.pi, self.SP.deviationLP * 180/np.pi ],
1355  autoScaleBottomX = self.autoScaleFreq )
1356 
1357  self.autoScaleFreq = False
1358 
1359  # Plot masks, or update if there are any
1360  if self.SM is not None:
1361  self.SPlotMagPhase.deleteMasks()
1362  self.plotMasks()
1363 
1364 
1365  ##
1366  #
1367  # Add masks to [S] plots.
1368  #
1369  def plotMasks(self):
1370  if self.SP is None: return # If self.SP does not exist, there are no results to plot yet.
1371 
1372  self.SM.specToMask(self.SP)
1373 
1374  maskNameRL = 'Return losses\nmask'
1375  maskNameRJ = 'Rejection\nmask'
1376  maskNameIL = 'Insertion losses\nmask'
1377  maskNameGDL = 'Group delay\nlower mask'
1378  maskNameGDU = 'Group delay\nupper mask'
1379 
1380  if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False and self.SPlotMagPhase.masks[0] == []: # Do not add masks if there are already
1381  if self.SM.S11mask is not None:
1382  self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1383  self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1384 
1385  if self.SM.S21outBandMask is not None:
1386  self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1387  self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1388 
1389  if self.SM.S21inBandMask is not None:
1390  self.SPlotMagPhase.addMask(nTab = 0, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1391  self.SPlotMagPhase.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1392 
1393  if self.SM.GDmaskLower is not None:
1394  self.SPlotMagPhase.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1395  self.SPlotMagPhase.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1396 
1397  self.SPlotMagPhase.changeCurveVisibility(0, [True, False, False], [False], { maskNameRL: True, maskNameRJ: False, maskNameIL: False } )
1398  self.SPlotMagPhase.changeCurveVisibility(1, [False, True, False], [False, False, False, False], { maskNameRL: False, maskNameRJ: True, maskNameIL: True} )
1399  self.SPlotMagPhase.changeCurveVisibility(2, [True], [False, False], { maskNameGDL: True, maskNameGDU: True } )
1400 
1401  if self.SPlotError is not None and self.SPlotError.closed is False and self.SPlotError.masks[0] == []: # Do not add masks if there are already
1402  if self.SM.S11mask is not None:
1403  self.SPlotError.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1404 
1405  if self.SM.S21outBandMask is not None:
1406  self.SPlotError.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1407 
1408  if self.SM.S21inBandMask is not None:
1409  self.SPlotError.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1410 
1411  if self.SM.GDmaskLower is not None:
1412  self.SPlotError.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = QColor(0, 175, 0), type = 'Lower', axis = 'left', transpar = 64 )
1413  self.SPlotError.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = QColor(0, 175, 0), type = 'Upper', axis = 'left', transpar = 64 )
1414 
1415  if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False and self.SPlotSensitivity.masks[0] == []: # Do not add masks if there are already
1416  if self.SM.S11mask is not None:
1417  self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1418  self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S11mask[:, 0]/1e9, YData = self.SM.S11mask[:, 1], name = maskNameRL, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 32 )
1419 
1420  if self.SM.S21outBandMask is not None:
1421  self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1422  self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S21outBandMask[:, 0]/1e9, YData =self.SM.S21outBandMask[:, 1], name = maskNameRJ, color = Qt.red, type = 'Upper', axis = 'left', transpar = 32 )
1423 
1424  if self.SM.S21inBandMask is not None:
1425  self.SPlotSensitivity.addMask(nTab = 0, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1426  self.SPlotSensitivity.addMask(nTab = 1, XData = self.SM.S21inBandMask[:, 0]/1e9, YData =self.SM.S21inBandMask[:, 1], name = maskNameIL, color = Qt.red, type = 'Lower', axis = 'left', transpar = 32 )
1427 
1428  if self.SM.GDmaskLower is not None:
1429  self.SPlotSensitivity.addMask(nTab = 2, XData = self.SM.GDmaskLower[:, 0]/1e9, YData =self.SM.GDmaskLower[:, 1]*1e9, name = maskNameGDL, color = Qt.blue, type = 'Lower', axis = 'left', transpar = 64 )
1430  self.SPlotSensitivity.addMask(nTab = 2, XData = self.SM.GDmaskUpper[:, 0]/1e9, YData =self.SM.GDmaskUpper[:, 1]*1e9, name = maskNameGDU, color = Qt.blue, type = 'Upper', axis = 'left', transpar = 64 )
1431 
1432  self.SPlotSensitivity.changeCurveVisibility(0, [True, False, False], [False], { maskNameRL: True, maskNameRJ: False, maskNameIL: False } )
1433  self.SPlotSensitivity.changeCurveVisibility(1, [False, True, False], [False, False, False], { maskNameRL: False, maskNameRJ: True, maskNameIL: True} )
1434  self.SPlotSensitivity.changeCurveVisibility(2, [True], [False], { maskNameGDL: True, maskNameGDU: True } )
1435 
1436 
1437  ##
1438  #
1439  # Update all plot windows and CM window if they exist.
1440  #
1441  def updateWindows(self):
1442  SP = self.SP
1443 
1444  if SP is None:
1446  self.closeCMWindows()
1447  else:
1448  if self.SPlotMagPhase is not None and not self.SPlotMagPhase.closed:
1449  self.plotSParameters()
1450 
1451  if self.SPlotError is not None and not self.SPlotError.closed:
1452  self.CMdialog.on_action_PlotS_triggered(True)
1453 
1454  if self.CMdialog is not None:
1455  self.CMdialog.close()
1456  self.CMdialog = CouplingMatrixDlg(self.CM, self)
1457  self.CMdialog.show()
1458 
1459  if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1460  self.CMdialog.on_action_PlotEnergy_triggered(True)
1461 
1462  if self.SPlotSensitivity is not None and not self.SPlotSensitivity.closed:
1463  self.CMdialog.on_runSensitivity_pushButton_clicked(True)
1464 
1465  if self.paramEdit is not None:
1466  self.paramEdit.P = self.P
1467  self.paramEdit.updateDialogParams()
1468 
1469 
1470  def addResultToList(self, resultArg):
1471  name = ''
1472  while len(name ) == 0:
1473  input = QInputDialog.getText (self , 'Result name', 'Give a name to current result:')
1474  if input[1] is False:
1475  return
1476  name = input[0]
1477  if len(name) == 0:
1478  QMessageBox.warning(self, "WARNING", 'Result name cannot be an empty string.')
1479 
1480  # BE CAREFUL: Add new elements to the tuple AT THE END.
1481  # Elements will be accessed later as SP = result[3]
1482  result = copy.deepcopy( resultArg )
1483 
1484  index = self.results_comboBox.findText(name, Qt.MatchExactly)
1485 
1486  if index >= 0: # name exists in list
1487  message = "Result with same name already exists in list\nReplace?"
1488  reply = QMessageBox.question(self, "%s - Result exists" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
1489  if reply == QMessageBox.No: # Exit without doing anything
1490  return False
1491  elif reply == QMessageBox.Yes:
1492  self.listResults[index] = result
1493  myPrint("Result '%s' replaced in list" % name)
1494  else: # Append result to list
1495  self.listResults.append(result)
1496  self.noCleanup = True
1497  self.results_comboBox.addItem(name) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1498  index = self.results_comboBox.count() - 1
1499  self.results_comboBox.setCurrentIndex(index) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1500  self.noCleanup = False
1501  self.indexResults = index
1502  myPrint("Result '%s' appended to list" % name)
1503 
1504 
1505  ##@{
1506  # @name Functions automatically excuted when the user interacts with the GUI
1507 
1508  ##
1509  #
1510  # Reimplementation of the window close function.
1511  #
1512  def closeEvent(self, event):
1513  # Check if there are unsaved changes and ask user what to do.
1514  if self.okToContinue():
1515  # Save window geometry and state
1516  self.settings = QSettings()
1517  self.settings.setValue("Geometry", QVariant(self.saveGeometry()))
1518  self.settings.setValue("State", QVariant(self.saveState()))
1519  # Save last open directory name
1520  self.settings.setValue('LastDirectoryOpened', QVariant(self.lastDir))
1521 
1522  # Accept event -> close window
1523  event.accept()
1524  else:
1525  # Ignore event -> do not close window
1526  event.ignore()
1527 
1528 
1529  @pyqtSignature("bool")
1530  ##
1531  #
1532  # Show "HelpAbout"" dialog from help menu.
1533  # This function is automatically executed by the GUI when the user selects the menu Help/About.
1534  # The information shown is read from the file docstring.
1535  #
1536  def on_action_HelpAbout_triggered(self, checked):
1537  st = __doc__
1538  st = st.replace('@package', 'Package')
1539  st = st.replace('@author', 'Authors:')
1540  st = st.replace('@date', 'Date:')
1541  st = st.replace('@version', 'Version:')
1542 
1543  st = u"""<b>Lossy Filters Synthesis</b>
1544  <p>%s
1545  <p>Python %s - Qt %s - PyQt %s - Numpy %s - Scipy %s - QWT %s on %s %s""" % (st, platform.python_version(),QT_VERSION_STR, PYQT_VERSION_STR,
1546  np.version.version, sc.version.version, Qwt.QWT_VERSION_STR, platform.system(), platform.release())
1547 
1548  QMessageBox.about(self, "About %s" % applicationName, st)
1549 
1550 
1551  @pyqtSignature("bool")
1552  ##
1553  #
1554  # Show "HelpHelp"" dialog from help menu.
1555  # This function is automatically executed by the GUI when the user selects the menu Help/Help.
1556  # The information shown is read file Help/GUI_help.html.
1557  #
1559  form = HelpForm("GUI_help.html", self)
1560  form.show()
1561  #QMessageBox.about(self, "Help %s" % applicationName, "Hello, no help for you")
1562 
1563 
1564  @pyqtSignature("bool")
1565  ##
1566  #
1567  # Close application.
1568  # This function is automatically executed by the GUI when the user triggers the FileExit action.
1569  #
1570  def on_action_FileExit_triggered(self, checked):
1571  self.close()
1572 
1573 
1574  @pyqtSignature("bool")
1575  ##
1576  #
1577  # Execute synthesis computation.
1578  # This function is automatically executed by the GUI when the user triggers the Execute action.
1579  #
1580  def on_action_Execute_triggered(self, checked):
1581 
1582  # Update self.P with values in the parameter editor, if it is open
1583  if self.paramEdit is not None and self.paramEdit.readDialogParams() == False:
1584  return # Do not compute with invalid parameters. The error message will be displayed by self.paramEdit.readDialogParams()
1585 
1586  if self.P is not None and self.P.empty is False:
1587  self.updateStatus("\nFilter synthesis started")
1588  app.setOverrideCursor(Qt.WaitCursor)
1589  self.CP, self.CM, self.SP, self.computed = runSynthesis(self.P, self.FT, symmetrizeZeros)
1590  app.restoreOverrideCursor()
1591 
1592  if self.computed:
1593  self.updateStatus("Filter synthesis finished\n")
1594 
1595  # Plot [S] parameters
1596  self.plotSParameters()
1597 
1598  # Reload coupling matrix window, if it was open.
1599  if self.CMdialog is not None:
1600  self.CMdialog.close()
1601  self.CMdialog = CouplingMatrixDlg(self.CM, self)
1602  self.CMdialog.show()
1603 
1604  # Update SPlotError, if it was open.
1605  if self.SPlotError is not None and not self.SPlotError.closed:
1606  if self.CMdialog is not None:
1607  self.CMdialog.on_action_PlotS_triggered(True)
1608  else:
1609  self.SPlotError.close()
1610 
1611  # Update EnergyPlot, if it was open.
1612  if self.EnergyPlot is not None and not self.EnergyPlot.closed:
1613  if self.CMdialog is not None:
1614  self.CMdialog.on_action_PlotEnergy_triggered(True)
1615  else:
1616  self.EnergyPlot.close()
1617  else:
1618  self.updateStatus("Filter synthesis aborted")
1619 
1620  else:
1621  self.updateStatus("There are no filter parameters loaded")
1622  QMessageBox.warning(self, "WARNING", 'There are no filter parameters loaded. Open a parameter file or create a new one.')
1623 
1624  if self.paramEdit is not None:
1625  # Reload parameters into GUI widgets, they may have been modified by the computation
1626  self.paramEdit.updateDialogParams(autoFreqUnits = False)
1627  self.paramEdit.recomputed = True
1628 
1629 
1630  @pyqtSignature("bool")
1631  ##
1632  #
1633  # Opens dialog to display and rotate coupling matrices.
1634  # This function is automatically executed by the GUI when the user triggers the 'Coupling Matrix' action.
1635  #
1637  # Open CouplingMatrix dialog
1638  if self.CM is not None:
1639  if self.CMdialog is None: self.CMdialog = CouplingMatrixDlg(self.CM, self)
1640  self.CMdialog.show()
1641  else:
1642  QMessageBox.warning(self, "WARNING", 'No available coupling matrix to display. Open a parameter file and run synthesis.')
1643 
1644 
1645  @pyqtSignature("bool")
1646  ##
1647  #
1648  # Opens dialog to edit parameters.
1649  # This function is automatically executed by the GUI when the user triggers the Edit action.
1650  #
1651  def on_action_Edit_triggered(self, checked):
1652  if self.paramEdit is not None:
1653  QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1654  else:
1655  # Open parameter editor dialog
1656  if self.P is not None:
1657  self.paramEdit = ParamEditDlg(self)
1658  self.paramEdit.show()
1659  self.paramEdit.raise_()
1660  #self.paramEdit.activateWindow()
1661  else:
1662  QMessageBox.warning(self, "WARNING", 'No open parameter file to edit. Open a parameter file or create a new one.')
1663 
1664 
1665  @pyqtSignature("bool")
1666  ##
1667  #
1668  # Save parameters to file.
1669  # This function is automatically executed by the GUI when the user triggers the FileSave action.
1670  #
1671  def on_action_FileSave_triggered(self, checked):
1672  if self.P is not None: # Check if there are parameters to save
1673  if self.fileName is not None: # Check if there is a filename
1674  # Open parameter file and write P on it. Use codecs module for transparent utf-8 encoding.
1675  fout = codecs.open(self.fileName, 'w', 'utf-8')
1676  fout.write( unicode(self.P) )
1677  fout.close()
1678  self.dirty = False
1679  self.setWindowModified(self.dirty)
1680  self.updateStatus("Parameters saved to file: %s" % self.fileName)
1681  else:
1682  self.on_action_FileSaveAs_triggered(False)
1683  else:
1684  QMessageBox.warning(self, "WARNING", "There are no parameters to save.")
1685 
1686  @pyqtSignature("bool")
1687  ##
1688  #
1689  # Ask for a file name and save parameters to file.
1690  # This function is automatically executed by the GUI when the user triggers the FileSaveAs action.
1691  #
1693  if self.P is not None: # Check if there are parameters to save
1694 
1695  # Set save directory to directory of current file
1696  dir = os.path.dirname(self.fileName) if self.fileName is not None else "."
1697 
1698  # Ask for file name
1699  formats = ["*.par" ]
1700  fname = unicode(QFileDialog.getSaveFileName(self, "%s - Save Parameter File" % applicationName, dir, "Parameter files (%s)" % " ".join(formats)))
1701 
1702  # If fname is valid, save parameters. Otherwise (File Save dialog cancelled by user) do nothing
1703  if fname:
1704  fbasename = os.path.basename(fname)
1705  if "." not in fbasename:
1706  fname += '.par'
1707  self.fileName = fname
1708  self.on_action_FileSave_triggered(False)
1709  else:
1710  QMessageBox.warning(self, "WARNING", "There are no parameters to save.")
1711 
1712  @pyqtSignature("bool")
1713  ##
1714  #
1715  # Ask for a file name and load parameters from this file.
1716  # This function is automatically executed by the GUI when the user triggers the FileOpen action.
1717  #
1718  def on_action_FileOpen_triggered(self, checked):
1719  if self.paramEdit is not None:
1720  QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1721  else:
1722  # Check if there are unsaved changes
1723  if not self.okToContinue():
1724  return
1725 
1726  # Cleaup results and plots
1727  self.cleanup()
1728 
1729  # Set open directory to directory of current file or to lastDir if there is no open file
1730  dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1731 
1732  # Open file
1733  formats = ["*.par *.dat" ]
1734  fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open Parameter File" % applicationName, dir, "Parameter files (%s)" % " ".join(formats)))
1735 
1736  # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
1737  if fname: self.fileRead(fname)
1738 
1739 
1740  @pyqtSignature("bool")
1741  ##
1742  #
1743  # If checked: Ask for a file name and load specification mask from this file. Plot the mask.
1744  # If unchecked: Delete the mask data from memory and delete the mask curve from the plots.
1745  # This function is automatically executed by the GUI when the user triggers the Mask action.
1746  #
1747  def on_action_Mask_triggered(self, checked):
1748  if checked:
1749  # Set open directory to directory of current .par file or to lastDir if there is no open file
1750  dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1751 
1752  # Open file
1753  formats = ["*.msk" ]
1754  fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open Mask Specification File" % applicationName, dir, "Mask spec files (%s)" % " ".join(formats)))
1755 
1756  if fname:
1757  self.action_Mask.setChecked(True) # Necessary when this method is called by the program, not the user
1758  self.SM = SpecMask(fname, self)
1759 
1760  # Plot mask if there is an open [S] plot window
1761  self.plotMasks()
1762 
1763  else: # Unckeck toolbar button
1764  self.action_Mask.setChecked(False)
1765 
1766 
1767  else:
1768  self.action_Mask.setChecked(False) # Necessary when this method is called by the program, not the user
1769  self.SM = None
1770 
1771  # Remove existing masks
1772  if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False:
1773  self.SPlotMagPhase.deleteMasks()
1774 
1775  if self.SPlotError is not None and self.SPlotError.closed is False:
1776  self.SPlotError.deleteMasks()
1777 
1778  if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False:
1779  self.SPlotSensitivity.deleteMasks()
1780 
1781  # Restore default curve visibility
1782  if self.SPlotMagPhase is not None and self.SPlotMagPhase.closed is False:
1783  self.SPlotMagPhase.changeCurveVisibility(0, [True, True, False], [True])
1784  self.SPlotMagPhase.changeCurveVisibility(1, [True, True, False], [True, False, False, False])
1785  self.SPlotMagPhase.changeCurveVisibility(2, [True], [True, False])
1786 
1787  if self.SPlotSensitivity is not None and self.SPlotSensitivity.closed is False:
1788  self.SPlotSensitivity.changeCurveVisibility(0, [True, True, False], [True])
1789  self.SPlotSensitivity.changeCurveVisibility(1, [True, True, False], [True, False, False])
1790  self.SPlotSensitivity.changeCurveVisibility(2, [True], [True])
1791 
1792 
1793  @pyqtSignature("bool")
1794  ##
1795  #
1796  # Ask for a file name and load [S] from this file in Touchstone format. Plot the [S] parameters in the results comparison window.
1797  # This function is automatically executed by the GUI when the user triggers the Sopen action.
1798  #
1799  def on_action_Sopen_triggered(self, checked):
1800 
1801  # Set open directory to directory of current .par file or to lastDir if there is no open file
1802  dir = os.path.dirname(self.fileName) if self.fileName is not None else self.lastDir
1803 
1804  # Open file
1805  formats = ["*.s2p" ]
1806  fname = unicode(QFileDialog.getOpenFileName(self, "%s - Open [S] Touchstone File" % applicationName, dir, "Touchstone files (%s)" % " ".join(formats)))
1807 
1808  # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
1809  if fname:
1810  Sts = Stouchstone(fname)
1811  self.addResultToList( (None, None, None, Sts, None) )
1812  self.on_action_Compare_triggered(True)
1813 
1814 
1815  @pyqtSignature("bool")
1816  ##
1817  #
1818  # Creates an empty instance of Parameters Class and opens dialog to edit parameters.
1819  # This function is automatically executed by the GUI when the user triggers the FileNew action.
1820  #
1821  def on_action_FileNew_triggered(self, checked):
1822  if self.paramEdit is not None:
1823  QMessageBox.warning(self, "WARNING", 'There is an open Parameter Edit Window. Close it before openning a new one.')
1824  else:
1825  # Check if there are unsaved changes
1826  if not self.okToContinue():
1827  return
1828 
1829  # Cleaup results and plots
1830  self.cleanup()
1831 
1832  self.P = Parameters(None)
1833  self.fileName = None
1834  self.updateStatus('New empty parameter set created')
1835  self.on_action_Edit_triggered(False)
1836 
1837 
1838  @pyqtSignature("bool")
1839  ##
1840  #
1841  # Store current result in self.results_comboBox list and self.listResults.
1842  # @param checked = SIGNAL/SLOT parameter.
1843  #
1844  def on_action_ListAdd_triggered(self, checked):
1845  if self.P is None:
1846  QMessageBox.warning(self, "WARNING", 'No result to store. Open a parameter file and run synthesis first.')
1847  return
1848 
1849  if self.CP is None or self.CM is None or self.SP is None or self.FT is None:
1850  QMessageBox.warning(self, "WARNING", 'No result to store. Run synthesis first.')
1851  return
1852 
1853  # If parameterEditDlg is open and not recomputed, compute with dialog parameters.
1854  if self.paramEdit is not None and self.paramEdit.dialogModified is True and self.paramEdit.recomputed is False:
1855  message = "Parameters have been modified and it is necessary to recompute\nRecompute synthesis?"
1856  reply = QMessageBox.question(self, "%s - Recompute" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
1857  if reply in [QMessageBox.Cancel, QMessageBox.No]:
1858  return
1859  elif reply == QMessageBox.Yes:
1860  self.paramEdit.on_compute_pushButton_clicked(True)
1861 
1862  self.addResultToList( (self.P, self.CP, self.CM, self.SP, self.FT) )
1863 
1864 
1865  @pyqtSignature("bool")
1866  ##
1867  #
1868  # Remove result matrix from self.results_comboBox and self.listResults lists.
1869  #
1871  if self.results_comboBox.count() == 0:
1872  QMessageBox.warning(self, "WARNING", 'Results list is empty' )
1873  return
1874 
1875  index = self.results_comboBox.currentIndex()
1876  name = str(self.results_comboBox.currentText())
1877 
1878  #print 'Remove result %d: %s' % (index, name)
1879 
1880  message = "Are you sure you what to remove result '%s' from list?\nRemove?" % name
1881  reply = QMessageBox.question(self, "%s - Remove result" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
1882 
1883  if reply == QMessageBox.Yes:
1884  self.results_comboBox.removeItem(index)
1885  del self.listResults[index]
1886  myPrint("Result '%s' removed from list" % name)
1887 
1888  if self.results_comboBox.count() > 0:
1889  # Load the result corresponding to the new index in CM_comboBox
1890  self.on_results_comboBox_currentIndexChanged(self.results_comboBox.currentIndex())
1891 
1892  self.indexResults = self.results_comboBox.currentIndex()
1893 
1894 
1895  @pyqtSignature("int")
1896  ##
1897  #
1898  # Load a new result.
1899  # This function is automatically executed by the GUI when the user changes the results list comboBox.
1900  # It does nothing if self.noCleanup is True.
1901  #
1903  # This method is automatically called when on_action_ListAdd_triggered() appends an item to the comboBox list, or when results_comboBox.setCurrentIndex() is called below.
1904  # self.noCleanup will be True. In that case we not want to do nohing.
1905  if self.noCleanup: return
1906 
1907  # Get result
1908  index = self.results_comboBox.currentIndex()
1909  name = str(self.results_comboBox.currentText())
1910  result = self.listResults[index]
1911 
1912  # Check if result has P, CP, CM, and FT. Otherwise it cannot be loaded.
1913  if not all([result[0], result[1], result[2], result[4] ]):
1914  QMessageBox.warning(self, "WARNING", """Result '%s' was read from [S] parameters Touchstone file and has no Characteristic Polynomials nor Coupling Matrices.<p>
1915  It cannot be loaded.""" % name)
1916  # Restore last index to comboBox
1917  self.noCleanup = True
1918  self.results_comboBox.setCurrentIndex(self.indexResults) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1919  self.noCleanup = False
1920  return
1921 
1922  # Check if parameters need saving.
1923  if not self.okToContinue():
1924  QMessageBox.warning(self, "WARNING", 'User cancelled action.<p>Result has not been loaded from list.' )
1925  # Restore last index to comboBox
1926  self.noCleanup = True
1927  self.results_comboBox.setCurrentIndex(self.indexResults) # Will call self.on_action_ListRemove_triggered, which calls self.cleanup()
1928  self.noCleanup = False
1929  return
1930 
1931  # Load results
1932  self.indexResults = index
1933 
1934  # BE CAREFUL: Add new elements to the tuple AT THE END.
1935  # Elements will be accessed later as SP = result[3]
1936  (self.P, self.CP, self.CM, self.SP, self.FT) = copy.deepcopy( result )
1937 
1938  # Update plots and edit windows if they are open.
1939  self.updateWindows()
1940 
1941  myPrint("Result '%s' loaded from list" % name)
1942 
1943  @pyqtSignature("bool")
1944  ##
1945  #
1946  # Compare results in list.
1947  # This function is automatically executed by the GUI when the user toggles the 'Compare results' button.
1948  #
1949  def on_action_Compare_triggered(self, checked):
1950  # Since this action does not update windows, it closes all windows first and, if checked, creates new windows.
1951  self.closeResultsWindow()
1952 
1953  if len(self.listResults) == 0:
1954  QMessageBox.warning(self, "WARNING", 'Results list is empty. Nothing to plot.')
1955  self.action_Compare.setChecked(False)
1956  return
1957 
1958  # Compute [S] parameters from current coupling matrix
1959  myPrint('Displayed results comparison:')
1960 
1961  # BE CAREFUL if somebody has inserted new elements in tuple
1962  freq = [ result[3].freq / 1e9 for result in self.listResults ]
1963  dataS11 = [ 20*np.log10(np.abs(result[3].S11)) for result in self.listResults ]
1964  dataS21 = [ 20*np.log10(np.abs(result[3].S21)) for result in self.listResults ]
1965 
1966  freqGD = [ result[3].freq2 / 1e9 for result in self.listResults ]
1967  dataGD = [ 1e9 * result[3].groupDelay2 for result in self.listResults ]
1968 
1969  phaseS11 = [ result[3].phaseS11 * 180/np.pi for result in self.listResults ]
1970  phaseS21 = [ result[3].phaseS21 * 180/np.pi for result in self.listResults ]
1971 
1972  resultNamesMag = [ unicode(self.results_comboBox.itemText(n) + ' (dB)') for n in range(self.results_comboBox.count()) ]
1973  resultNamesPhase = [ unicode(self.results_comboBox.itemText(n) + ' (phase)') for n in range(self.results_comboBox.count()) ]
1974  resultNamesGD = [ unicode(self.results_comboBox.itemText(n) + ' (GD)') for n in range(self.results_comboBox.count()) ]
1975  rightYVisible = [ False for n in range(self.results_comboBox.count())]
1976 
1977  # Set dynamic range to the lowest value of S21 at sweep range ends.
1978  dynamicRange = 0
1979  for S21 in dataS21: dynamicRange = max(dynamicRange , -20*log10( min([ abs(S21[0]), abs(S21[-1] )])) )
1980  dynamicRange = 10*ceil(dynamicRange/10)
1981 
1982  self.SPlotCompare = DbPlot(parent=self,
1983  windowTitle = '[S] comparison',
1984  tabTitle = 'S11',
1985  title = 'S<sub>11</sub> compared for different results',
1986  XData = freq, leftYData = dataS11,
1987  rightYData = phaseS11,
1988  rightYLabel = 'Phase(S<sub>11</sub>) (deg)' , rightYNames = resultNamesPhase, rightYVisible = rightYVisible,
1989  leftYmax = 0, leftYmin = -dynamicRange, leftYNames = resultNamesMag, XLabel = 'Frequency (GHz)', leftYLabel = 'dB',
1990  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg' )
1991 
1992  self.SPlotCompare.addTab(tabTitle = 'S21',
1993  title = 'S<sub>21</sub> compared for different results',
1994  XData = freq, leftYData = dataS21,
1995  rightYData = phaseS21,
1996  rightYLabel = 'Phase(S<sub>21</sub>) (deg)' , rightYNames = resultNamesPhase, rightYVisible = rightYVisible,
1997  leftYmax = 0, leftYmin = -dynamicRange, leftYNames = resultNamesMag,
1998  XLabel = 'Frequency (GHz)', leftYLabel = 'dB',
1999  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg' )
2000 
2001  self.SPlotCompare.addTab(tabTitle = 'Group delay',
2002  title = 'Group delay compared for different results',
2003  XData = freqGD, leftYData = dataGD,
2004  leftYNames = resultNamesGD, XLabel = 'Frequency (GHz)', leftYLabel = 'Group delay (ns.)',
2005  Xunits = 'GHz', leftYunits = 'ns' )
2006 
2007 
2008 ##@}
2009 
2010 # End class: MainWindow #
2011 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2012 
2013 
2014 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2015 # Begin class: HelpForm
2016 
2017 ##
2018 #
2019 # GUI dialog to display help
2020 #
2021 class HelpForm(QDialog):
2022 
2023  ##
2024  #
2025  # Constructor: Creates dialog window to display html help file.
2026  # @param page = html file name, relative to the program folder.
2027  # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2028  #
2029  def __init__(self, page, parent=None):
2030  super(HelpForm, self).__init__(parent)
2031  self.setAttribute(Qt.WA_DeleteOnClose)
2032  self.setAttribute(Qt.WA_GroupLeader)
2033 
2034  ##
2035  # Copy of the mainWindow class instance.
2036  # It is necessary to store a copy since it must be used to access actions and resources.
2037  self.mainWindow = parent
2038 
2039 # These actions have been already created in Qt Designer
2040 # backAction = QAction(QIcon(":/back.png"), self.tr("&Back"), self)
2041 # homeAction = QAction(QIcon(":/home.png"), self.tr("&Home"), self)
2042  backAction = self.mainWindow.action_Back
2043  homeAction = self.mainWindow.action_Home
2044 
2045  backAction.setShortcut(QKeySequence.Back)
2046  homeAction.setShortcut(self.tr("Home"))
2047  self.pageLabel = QLabel()
2048 
2049  toolBar = QToolBar()
2050  toolBar.addAction(backAction)
2051  toolBar.addAction(homeAction)
2052  toolBar.addWidget(self.pageLabel)
2053  self.textBrowser = QTextBrowser()
2054 
2055  layout = QVBoxLayout()
2056  layout.addWidget(toolBar)
2057  layout.addWidget(self.textBrowser, 1)
2058  self.setLayout(layout)
2059 
2060  self.connect(backAction, SIGNAL("triggered()"), self.textBrowser, SLOT("backward()"))
2061  self.connect(homeAction, SIGNAL("triggered()"), self.textBrowser, SLOT("home()"))
2062  self.connect(self.textBrowser, SIGNAL("sourceChanged(QUrl)"), self.updatePageTitle)
2063 
2064  self.textBrowser.setSearchPaths([":/help/Help"]) # Path ":/" is the resource file root, path "./" is directory where the application was called
2065  self.textBrowser.setSource(QUrl(page))
2066  self.resize(1024, 700)
2067  self.setWindowTitle(self.tr("%1 Help").arg(QApplication.applicationName()))
2068 
2069 
2070  def updatePageTitle(self):
2071  self.pageLabel.setText(self.textBrowser.documentTitle())
2072 
2073 # End class: HelpForm
2074 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2075 
2076 ##
2077 #
2078 # Dialog to set the type of a coupling.
2079 #
2080 class SetCouplingDlg(QDialog):
2081 
2082  ##
2083  #
2084  # Create dialog to set the type of a coupling.
2085  # @param item = Topology table item to modify
2086  # @param topolText = Topology table text, corresponding to ComboBox items
2087  # @param topolColors = Topology table colors, corresponding to ComboBox items
2088  # @param parent = parent widget (topology table)
2089  #
2090  def __init__(self, item, topolText, topolColors, parent=None):
2091  super(SetCouplingDlg, self).__init__(parent)
2092  self.setAttribute(Qt.WA_DeleteOnClose)
2093 
2094  ##
2095  # Topology table widget
2096  self.mainWindow = parent
2097 
2098  ##
2099  # Topology table item to set
2100  self.item = item
2101 
2102  ##
2103  # ComboBox item text for different kinds of coupling
2104  self.typeLabels = ['Uncoupled', 'Real', 'Real+', 'Real-', 'Imag+', 'Complex', 'Complex+', 'Complex-']
2105 
2106  ##
2107  # Topology table text, corresponding to ComboBox items
2108  self.topolText = topolText
2109 
2110  ##
2111  # Topology table color, corresponding to ComboBox items
2112  self.topolColors = topolColors
2113 
2114  self.label = QLabel('Type of coupling:')
2115  self.comboBox = QComboBox(self)
2116  self.comboBox.addItems(self.typeLabels)
2117 
2118  # Set table item value as comboBox current item
2119  entriesDict = dict(zip(self.topolText, self.typeLabels))
2120  self.comboBox.setCurrentIndex( self.comboBox.findText( entriesDict[ str(item.text()) ] ) )
2121 
2122  layout = QVBoxLayout(self)
2123  layout.addWidget(self.label)
2124  layout.addWidget(self.comboBox)
2125 
2126  self.setWindowTitle("Cross couplings")
2127  self.setToolTip("""Choose the type of cross coupling:
2128 The real part is a conventional (inductive or capacitive)
2129 coupling and the imaginary part a resistive coupling.
2130 The imag part will always be positive (passive circuit),
2131 while the real part can forced to positive, negative or
2132 have any sign.""")
2133 
2134  self.connect(self.comboBox, SIGNAL("currentIndexChanged(int)"), self.typeSelected)
2135 
2136 
2137  ##
2138  #
2139  # Set type selected in self.comboBox to self.item
2140  #
2141  def typeSelected(self, index):
2142  row = self.mainWindow.topology_tableWidget.currentRow()
2143  col = self.mainWindow.topology_tableWidget.currentColumn()
2144 
2145  if row != col: # Do not change main diagonal
2146  itemReciprocal = self.mainWindow.topology_tableWidget.item(col, row)
2147  for it in [self.item, itemReciprocal]:
2148  it.setText(self.topolText[index])
2149  it.setBackground(QBrush(QColor(self.topolColors[index])))
2150 
2151  self.close()
2152 
2153 
2154 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2155 # Begin class: MatrixEditDlg
2156 
2157 ##
2158 #
2159 # GUI dialog to edit coupling matrix
2160 #
2161 class MatrixEditDlg(QDialog, Ui_matrixeditdlg.Ui_MatrixEditDlg):
2162 
2163  ##
2164  #
2165  # Constructor: Creates dialog window to edit current coupling matrix.
2166  # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2167  #
2168  def __init__(self, parent):
2169  super(MatrixEditDlg, self).__init__(parent)
2170  # Caution: do not set Qt.WA_DeleteOnClose attribute. The class instance must not be deleted when closing the dialog.
2171 
2172  ##
2173  # Copy of the coupling matrix mainWindow class instance.
2174  # It is necessary to store a copy since it must be accessed by some member functions.
2175  self.mainWindow = parent
2176 
2177  self.setupUi(self)
2178 
2179  ##
2180  # String that explains the last edit action. This is what is pushed into the edit action list after edit actions.
2181  self.lastEditAction = ''
2182 
2183  ##
2184  # Shallow copy of the CM.bkpMatQ list, for easier access.
2185  self.bkpMatQ = self.mainWindow.CM.bkpMatQ
2186 
2187  ##
2188  # Shallow copy of the CM.bkpActionslist, for easier access.
2189  self.bkpActions = self.mainWindow.CM.bkpActions
2190 
2191  # Initialize edit list with previous list of edit actions
2192  assert self.edit_listWidget.count() == 0 # List should be empty here. We check it because list is not destructed on close.
2193  self.edit_listWidget.insertItems(0, self.bkpActions)
2194 
2195  ##
2196  # Variable to store a list of widgets with bad input.
2197  # Only when this list is empty, he "Accept" and "Compute" buttons are enabled.
2198  self.badInput = [ ]
2199 
2200  # Set maximum row and column indices in matrixEditDlg spinBoxes
2201  self.setMatrixSize()
2202 
2203  # Prepare rotation angle lineEdit. Initial value equal to zero and cannot be empty.
2204  self.rot_angle_lineEdit.setText('0')
2205  self.rot_angle_lineEdit.setValidator( QRegExpValidator( QRegExp(stComplex), self) )
2206 
2207  # Prepare optimization Q. Initial value equal to zero and cannot be empty.
2208  self.Q_doubleSpinBox.setValue( np.mean(np.abs(self.mainWindow.CM.MatQ.Q)) )
2209 
2210  # Prepare Qeff for prescribed flatness
2211  self.flatnessQeff_lineEdit.setValidator( QRegExpValidator( reRealPositiveInf, self) )
2212 
2213  # Optimization comboBox and stackedWidget
2214  optimMethod = 0
2215  self.optim_comboBox.setCurrentIndex(optimMethod)
2216  self.optim_stackedWidget.setCurrentIndex(optimMethod)
2217 
2218  # Matrix topology table
2219  self.topology_tableWidget = myTableWidget(name = "topology", parent = self.optim_page)
2220  self.topology_tableWidget.setColumnCount(0)
2221  self.topology_tableWidget.setRowCount(0)
2222 
2223  self.topology_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
2224  self.topology_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
2225 
2226  self.topology_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
2227 
2228  self.topology_tableWidget.setToolTip("""<b>Coupling matrix topology: </b>
2229 <p>
2230 Define here the position of purely real, purely imaginary
2231 or complex non-diagonal coupling matrix entries.
2232 <p>
2233 Diagonal entries corresponding to resonant nodes are always complex, with imaginary
2234 part determined by finite Q of resonators and real part a degree of freedom.
2235 <p>
2236 Diagonal entries corresponding to non-resonant nodes are always purely imaginary negative.
2237 <p>
2238 Real part of non-diagonal entries can be forced to be positive (R+ or C+)
2239 or negative (R- or C-), to have a electric or a magnetic coupling.
2240 <p>
2241 Imaginary part of complex or purely imaginary non-diagonal entries is always positive
2242 to have a passive circuit.
2243 """)
2244 
2245  self.verticalLayout_13.addWidget(self.topology_tableWidget, 100) # stretchFactor=100
2246  self.verticalLayout_13.addStretch()
2247 
2248  # Make topology table as large as possible
2249  self.verticalLayout_13.setStretchFactor(self.topology_tableWidget, 100)
2250 
2251  # Make edit toolBox as large as possible, to fit the topology table
2252  self.verticalLayout_12.setStretchFactor(self.edit_toolBox, 10)
2253 
2254  # Connect slot
2255  self.connect(self.topology_tableWidget, SIGNAL("itemClicked(QTableWidgetItem*)"), self.topologyItemClicked)
2256 
2257  ##
2258  # Text corresponding to couplings type in topology table
2259  self.topolText = [ '0', 'R', 'R+', 'R-', 'I+', 'C', 'C+', 'C-', '']
2260 
2261  ##
2262  # Colors corresponding to couplings type in topology table
2263  self.topolColors = [ Qt.white, Qt.blue, Qt.green, Qt.cyan, Qt.yellow, Qt.darkRed, Qt.red, Qt.magenta, Qt.black ]
2264 
2265  # Fill in Matrix topology table
2266  self.udpateTopology()
2267 
2268  ##
2269  # Variable to store the modified/unmodifed status of the dialog widgets.
2270  # The dialogChanged() method is automatically called when the user modifies the edit widgets contents and sets this variable to True.
2271  # This variable must be set to False at the end of the __init__ dialog constructor,
2272  # since dialogChanged() is called when the constructor sets the edit widgets contents.
2273  # This attribute is defined in this class only for compatibility with the dialogChanged() method,
2274  # which has been copied literally (except for the name of pushButtons) from ParamEditDlg class.
2275  self.dialogModified = False
2276 
2277 
2278  ##
2279  #
2280  # Fill in Matrix topology table from values in self.mainWindow.CM.MatQ
2281  #
2282  def udpateTopology(self):
2283  table = self.topology_tableWidget
2284  labels = self.mainWindow.rowIndices
2285 
2286  M = self.mainWindow.CM.MatQ.M
2287 
2288  size = len(labels)
2289  table.setColumnCount(size)
2290  table.setRowCount(size)
2291  table.setHorizontalHeaderLabels( labels )
2292  table.setVerticalHeaderLabels( labels )
2293 
2294  # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
2295 # eps = self.mainWindow.CM.matrixElementsEps('Matrix topology')
2296  eps = 10** -self.mainWindow.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
2297 
2298  for m in range(size):
2299  for n in range(size):
2300  if n != m:
2301  if np.abs(M[m, n].real) < eps:
2302  if np.abs(M[m, n].imag) < eps: type = 0 # Uncoupled
2303  else: type = 4 # Imaginary (positive)
2304  else:
2305  if np.abs(M[m, n].imag) < eps:
2306  if M[m, n].real > 0: type = 2 # Real positive
2307  else: type = 3 # Real negative
2308  else:
2309  if M[m, n].real > 0: type = 6 # Complex positive
2310  else: type = 7 # Complex negative
2311  else: type = 8 # Main diagonal
2312 
2313  st = self.topolText[type]
2314  brush = self.topolColors[type]
2315  item = QTableWidgetItem(st)
2316  item.setTextAlignment(Qt.AlignHCenter|Qt.AlignVCenter)
2317  item.setBackground(brush)
2318 
2319 # # This is set for the whole table in MatrixEditDlg.__init__()
2320 # if n != m:
2321 # item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable)
2322 # else:
2323 # item.setFlags( Qt.NoItemFlags )
2324 
2325  table.setItem(m, n, item)
2326 
2327  table.resizeRowsToContents()
2328  table.resizeColumnsToContents()
2329  table.setMaximumSize( table.maximumSizeHint() )
2330 
2331 
2332  ##
2333  #
2334  # Return matrices of flags that describe the topology of the real part and the imaginary part of the coupling matrix,
2335  # based on the entries in the topology table.
2336  # @return topologyReal = Numpy matrix of Bool type that has True values for elements with non-zero real part.
2337  # @return topologyImag = Numpy matrix of Bool type that has True values for elements with non-zero imag part.
2338  # @return topologySignReal = Int numpy array having -1,1 or 0 values at the position of coupling matrix entries with negative, positive or any real part, respectively.
2339  # @return topologySignImag = Int numpy array having -1,1 or 0 values at the position of coupling matrix entries with negative, positive or any imaginary part, respectively.
2340  #
2341  def getTopologyFlags(self):
2342  table = self.topology_tableWidget
2343  Nrows = table.rowCount()
2344  Ncols = table.columnCount()
2345  assert Nrows == Ncols
2346 
2347  topologyReal = np.zeros( (Nrows,Ncols), dtype = bool)
2348  topologyImag = np.zeros( (Nrows,Ncols), dtype = bool)
2349  topologySignReal = np.zeros((Nrows,Ncols), dtype='int') # Int numpy array having -1,1 or 0 values at the position of coupling matrix entries that must be negative, positive or any, respectively.
2350  topologySignImag = np.zeros((Nrows,Ncols), dtype='int') # Int numpy array having -1,1 or 0 values at the position of coupling matrix entries that must be negative, positive or any, respectively.
2351 
2352  for m in range(Nrows):
2353  for n in range(Ncols):
2354  if n==m: continue # Diagonal elements are not set in the GUI
2355  st = str(table.item(m, n).text())
2356  type = self.topolText.index(st)
2357  topologyReal[m, n] = type in [1, 2, 3, 5, 6, 7]
2358  topologyImag[m, n] = type in [4, 5, 6, 7]
2359  if topologyImag[m, n]: topologySignImag[m, n] = 1 # All non-diagonal imaginary parts must be positive to have a passive filter.
2360  if type in [2, 6]: topologySignReal[m, n] = 1 # Positive real part
2361  elif type in [3, 7]: topologySignReal[m, n] = -1 # Negative real part
2362  elif type in [1, 5]: topologySignReal[m, n] = 0 # Any real part
2363  else: topologySignReal[m, n] = 0 # Zero real part
2364 
2365  return topologyReal, topologyImag, topologySignReal, topologySignImag
2366 
2367  ##
2368  #
2369  # Update the contents of self.topology_tableWidget after the user has clicked an item.
2370  #
2371  def topologyItemClicked(self, item):
2372  row = self.topology_tableWidget.currentRow()
2373  col = self.topology_tableWidget.currentColumn()
2374  self.topology_tableWidget.setFocus(Qt.MouseFocusReason)
2375 
2376  if row != col: # Do not change main diagonal
2377  couplingsDlg = SetCouplingDlg(item, self.topolText, self.topolColors, self)
2378  couplingsDlg.exec_()
2379 
2380 
2381  ##
2382  #
2383  # Set maximum row and column indices in matrixEditDlg spinBoxes, according to size of self.mainWindow.CM.MatQ.M.
2384  #
2385  def setMatrixSize(self):
2386  # Set row and column indices comboBoxes
2387  (M, N) = self.mainWindow.CM.MatQ.M.shape
2388 
2389  ind = self.rot_pivotM_comboBox.currentIndex()
2390  self.rot_pivotM_comboBox.clear()
2391  self.rot_pivotM_comboBox.addItems(self.mainWindow.rowIndices)
2392  self.rot_pivotM_comboBox.setCurrentIndex(ind)
2393 
2394  ind = self.rot_pivotN_comboBox.currentIndex()
2395  self.rot_pivotN_comboBox.clear()
2396  self.rot_pivotN_comboBox.addItems(self.mainWindow.rowIndices)
2397  self.rot_pivotN_comboBox.setCurrentIndex(ind)
2398 
2399  ind = self.ann_pivotM_comboBox.currentIndex()
2400  self.ann_pivotM_comboBox.clear()
2401  self.ann_pivotM_comboBox.addItems(self.mainWindow.rowIndices)
2402  self.ann_pivotM_comboBox.setCurrentIndex(ind)
2403 
2404  ind = self.ann_pivotN_comboBox.currentIndex()
2405  self.ann_pivotN_comboBox.clear()
2406  self.ann_pivotN_comboBox.addItems(self.mainWindow.rowIndices)
2407  self.ann_pivotN_comboBox.setCurrentIndex(ind)
2408 
2409  ind = self.ann_elemM_comboBox.currentIndex()
2410  self.ann_elemM_comboBox.clear()
2411  self.ann_elemM_comboBox.addItems(self.mainWindow.rowIndices)
2412  self.ann_elemM_comboBox.setCurrentIndex(ind)
2413 
2414  ind = self.ann_elemN_comboBox.currentIndex()
2415  self.ann_elemN_comboBox.clear()
2416  self.ann_elemN_comboBox.addItems(self.mainWindow.rowIndices)
2417  self.ann_elemN_comboBox.setCurrentIndex(ind)
2418 
2419  ind = self.scale_node_comboBox.currentIndex()
2420  self.scale_node_comboBox.clear()
2421  self.scale_node_comboBox.addItems(self.mainWindow.rowIndices)
2422  self.scale_node_comboBox.setCurrentIndex(ind)
2423  ##@{
2424  # @name Functions automatically excuted when the user interacts with the GUI
2425 
2426 
2427  ##
2428  #
2429  # Custom slot in Qt designer connected to widgets "changed" signal.
2430  # This function is automatically executed by the GUI every time the user changes dialog values,
2431  # and sets the self.dialogModified variable to True.
2432  #
2433  def dialogChanged(self):
2434  self.dialogModified = True
2435  if hasattr(self.sender(), 'validator'):
2436  validator = self.sender().validator()
2437  if validator is not None:
2438  result = validator.validate(self.sender().text(), 0)[0]
2439  palette = self.sender().palette()
2440  if result == 1:
2441  palette.setColor(QPalette.Text, Qt.red)
2442  self.sender().setPalette(palette)
2443  self.badInput.append(self.sender())
2444  self.sender().setFocus(Qt.OtherFocusReason) # It does nothing, because it is executed before focus changes to another widget.
2445  if result == 2:
2446  palette.setColor(QPalette.Text, self.mainWindow.palette().text().color())
2447  self.sender().setPalette(palette)
2448  while self.sender() in self.badInput: self.badInput.remove(self.sender())
2449 
2450  # For debugging
2451  #print [ unicode(n.text()) for n in self.badInput]
2452 
2453  if len(self.badInput) > 0:
2454  self.apply_pushButton.setEnabled(False)
2455  self.undo_pushButton.setEnabled(False)
2456  else:
2457  self.apply_pushButton.setEnabled(True)
2458  self.undo_pushButton.setEnabled(True)
2459 
2460  @pyqtSignature("QListWidgetItem*")
2461  ##
2462  #
2463  # Load coupling matrix saved in backup list
2464  #
2466  st = item.text()
2467  index = self.edit_listWidget.row(item)
2468  self.mainWindow.CM.MatQ = self.bkpMatQ[index+1].copy() # Deep copy because MatQ will be modified by edit actions. The first matrix in list was loaded before any edit actions.
2469  self.mainWindow.loadMatrixQ(self.mainWindow.CM.MatQ)
2470  self.lastEditAction = "Matrix reverted to backup state: '%s'" % st
2471  self.mainWindow.pushEditAction()
2472 
2473 
2474  @pyqtSignature("bool")
2475  ##
2476  #
2477  # Undo last edit action from edit actions list. Pops the edit action list and the backup Qmatrices list.
2478  #
2479  def on_undo_pushButton_clicked(self, checked):
2480  # Number of edit actions saved
2481  count = self.edit_listWidget.count()
2482  assert count == len( self.bkpMatQ)-1
2483 
2484  if count == 0:
2485  QMessageBox.warning(self, "WARNING", "There are no edit actions to undo.")
2486  else:
2487  badMatQ = self.bkpMatQ.pop() # Discard last modified matrix from the backup list
2488  del badMatQ # Just in case garbage collection fails.
2489  MatQ = self.bkpMatQ[-1].copy() # Get the last valid matrix. Deep copy because MatQ will be modified by edit actions.
2490  self.mainWindow.CM.MatQ = MatQ # Update the CM.MatQ attribute with reference to the new MatQ.
2491  self.mainWindow.loadMatrixQ(MatQ) # Load the new MatQ in table widgets.
2492  item = self.edit_listWidget.takeItem(count-1) # Remove last edit action from edit actions list.
2493  self.bkpActions.pop()
2494  myPrint("Undo: Matrix Edit action '%s'" % item.text())
2495  del item # According to Qt4 doc, taken items must be deleted manually.
2496 
2497 
2498  @pyqtSignature("bool")
2499  ##
2500  #
2501  # Clear edit actions list. Clears edit action list and the backup Qmatrices list.
2502  #
2503  def on_clear_pushButton_clicked(self, checked):
2504 
2505  message = "Do you really want to clear the action list for undo?"
2506  reply = QMessageBox.question(self, "%s - Clear actions list" % applicationName, message, QMessageBox.Yes | QMessageBox.Cancel)
2507  if reply == QMessageBox.Cancel:
2508  return
2509 
2510  self.mainWindow.CM.bkpMatQ = [ self.mainWindow.CM.MatQ.copy() ]
2511  self.mainWindow.CM.bkpActions = [ ]
2512 
2513  self.bkpMatQ = self.mainWindow.CM.bkpMatQ
2514  self.bkpActions = self.mainWindow.CM.bkpActions
2515 
2516  # Remove all list items
2517  while self.edit_listWidget.count() > 0:
2518  item = self.edit_listWidget.takeItem(0)
2519  del item
2520 
2521 
2522  @pyqtSignature("bool")
2523  ##
2524  #
2525  # Apply selected action to the matrix.
2526  # The coupling matrix self.dirty attribute is set to True.
2527  #
2528  def on_apply_pushButton_clicked(self, checked):
2529  MatQ = self.mainWindow.CM.MatQ
2530 
2531  try:
2532  action = unicode(self.edit_toolBox.itemText(self.edit_toolBox.currentIndex()))
2533  if action == u'Rotate matrix':
2534  if self.rot_trigo_radioButton.isChecked(): rotationType = 'trigonometric'
2535  elif self.rot_hyper_radioButton.isChecked(): rotationType = 'hyperbolic'
2536  else: raise synthError, "No rotation type selected"
2537  i = self.rot_pivotM_comboBox.currentIndex()
2538  j = self.rot_pivotN_comboBox.currentIndex()
2539  rotAng = complex( str(self.rot_angle_lineEdit.text()).replace(' ', '').replace('i', 'j') ) *pi/180.0 # Rotate angle is in deg in the GUI
2540 # (M, N) = MatQ.M.shape
2541 # if i>=M or j>=N: raise synthError, 'Pivot indices (%d,%d) larger than matrix size' % (i, j)
2542  MatQ.rotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2543 # MatQ.mpRotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2544  self.lastEditAction = '%s matrix rotation with pivot (%s,%s) and angle %g %+gj deg' % (rotationType.title(), self.mainWindow.rowIndices[i], self.mainWindow.rowIndices[j], rotAng.real *180/pi, rotAng.imag*180/pi ) # Rotate angle is in deg in the GUI
2545 
2546  elif action == 'Annihilate element':
2547  if self.ann_trigo_radioButton.isChecked(): rotationType = 'trigonometric'
2548  elif self.ann_hyper_radioButton.isChecked(): rotationType = 'hyperbolic'
2549  else: raise synthError, "No rotation type selected"
2550  i = self.ann_pivotM_comboBox.currentIndex()
2551  j = self.ann_pivotN_comboBox.currentIndex()
2552  m = self.ann_elemM_comboBox.currentIndex()
2553  n = self.ann_elemN_comboBox.currentIndex()
2554 # (M, N) = MatQ.M.shape
2555 # if i>=M or j>=N: raise synthError, 'Pivot indices (%d,%d) larger than matrix size' % (i, j)
2556 # if m>=M or n>=N: raise synthError, 'Element indices (%d,%d) larger than matrix size' % (i, j)
2557  rotAng = MatQ.rotAngleEliminate(rotationType, i, j, m, n)
2558  MatQ.rotateMatrix(rotationType, i, j, rotAng, flagQ=True)
2559 
2560  self.lastEditAction = '%s matrix rotation with pivot (%s,%s) and angle %g %+gj deg to annihilate element (%s,%s)' % (rotationType.title(), self.mainWindow.rowIndices[i], self.mainWindow.rowIndices[j], rotAng.real *180/pi, rotAng.imag*180/pi, self.mainWindow.rowIndices[m], self.mainWindow.rowIndices[n]) # Rotate angle is in deg in the GUI
2561 
2562  elif action == 'Add nodes':
2563  if self.add_source_radioButton.isChecked():
2564  position = 'source'
2565  self.lastEditAction = 'Node added at source side'
2566  elif self.add_load_radioButton.isChecked():
2567  position = 'load'
2568  self.lastEditAction = 'Node added at load side'
2569  elif self.add_both_radioButton.isChecked():
2570  position = 'both'
2571  self.lastEditAction = 'Nodes added at both sides'
2572  else: raise synthError, "Add nodes: No new node position selected"
2573  MatQ.inflateMatrix(position)
2574 
2575  elif action == 'Node scaling':
2576  factor = self.scale_factor_doubleSpinBox.value()
2577  node = self.scale_node_comboBox.currentIndex()
2578  MatQ.scaleNode(node, factor)
2579  self.lastEditAction = 'Node [%s] scaled with factor %g' % (self.mainWindow.rowIndices[node], factor)
2580 
2581  elif action == 'Prescribed flatness':
2582  Qeff = float(self.flatnessQeff_lineEdit.text())
2583  MatQ.addLossesQeff(Qeff)
2584  self.lastEditAction = 'Prescribed flatness with Qeff = %g' % (Qeff)
2585 
2586  elif action == 'Optimization':
2587 
2588  Q = self.Q_doubleSpinBox.value()
2589  topologyReal, topologyImag, topologySignReal, topologySignImag = self.getTopologyFlags()
2590 
2591  algor = self.algor_comboBox.currentText()
2592  maxIter = self.maxIter_spinBox.value()
2593  Nsamples = self.Nsamples_spinBox.value()
2594  weights = [ self.wS11_doubleSpinBox.value(), self.wS22_doubleSpinBox.value(), self.wS21_doubleSpinBox.value(), self.wGD_doubleSpinBox.value() ]
2595 
2596  optimMethod = unicode(self.optim_comboBox.currentText())
2597  if optimMethod == u'Characteristic Polynomials':
2598  S11DR = self.S11DR_doubleSpinBox.value()
2599  S21DR = self.S21DR_doubleSpinBox.value()
2600  app.setOverrideCursor(Qt.WaitCursor)
2601  MatQ.uniformQ(self.mainWindow.mainWindow.P, self.mainWindow.mainWindow.CP, weights, Q, algor, maxIter, Nsamples, S11DR, S21DR, topologyReal, topologyImag, topologySignReal, topologySignImag)
2602  app.restoreOverrideCursor()
2603  self.lastEditAction = 'Matrix optimization (CP) with parameters: alg=%s, maxIter=%d, Nsamples=%d, Q=%g, w=%s, N=%d, S11DR=%g, S21DR=%g' % (algor, maxIter, Nsamples, Q, listStr(weights, 2), Nsamples, S11DR, S21DR)
2604 
2605  elif optimMethod == u'Specification mask':
2606  SM = self.mainWindow.mainWindow.SM
2607 
2608  if SM is None:
2609  reply = QMessageBox.question(self, "WARNING", 'No specification mask loaded.<p>Do you want to open an specification mask file now?', QMessageBox.Yes| QMessageBox.Cancel)
2610  if reply == QMessageBox.Yes:
2611  self.mainWindow.mainWindow.on_action_Mask_triggered(True)
2612  SM = self.mainWindow.mainWindow.SM
2613 
2614  if SM is not None:
2615  app.setOverrideCursor(Qt.WaitCursor)
2616  MatQ.uniformQMask(self.mainWindow.mainWindow.P, self.mainWindow.mainWindow.CP, weights, Q, algor, maxIter, Nsamples, SM, topologyReal, topologyImag, topologySignReal, topologySignImag)
2617  app.restoreOverrideCursor()
2618  self.lastEditAction = 'Matrix optimization (mask) with parameters: alg=%s, maxIter=%d, Nsamples=%d, Q=%g, w=%s, mask=%s' % (algor, maxIter, Nsamples, Q, listStr(weights, 2), SM.fileName)
2619  else:
2620  myPrint('Matrix optimization (mask) cancelled by user')
2621  return # Matrix edit action has been canceled and we do not want to run the code below
2622 
2623  else:
2624  QMessageBox.warning(self, "WARNING", 'Unknown optimization method: %s' % optimMethod)
2625 
2626 
2627 
2628  else:
2629  raise synthError, "Unknown matrix edit action '%s'" % action
2630 
2631  MatQ.Qresonators()
2632  self.mainWindow.loadMatrixQ(MatQ)
2633  self.mainWindow.pushEditAction()
2634  self.mainWindow.dirty = True
2635  self.mainWindow.setWindowModified(self.mainWindow.dirty)
2636 
2637  except synthError, err:
2638  app.restoreOverrideCursor()
2639  QMessageBox.critical(self, "ERROR", '<p align="center">Matrix editor error:<br>%s</p>' % err )
2640  return
2641 
2642 
2643  ##
2644  #
2645  # Reimplementation of the window close function.
2646  # Sets couplingMatrixDlg toolbar Edit action to unchecked.
2647  #
2648  def closeEvent(self, event):
2649  self.mainWindow.action_Edit.setChecked(False)
2650  event.accept()
2651 
2652  ##@}
2653 
2654 
2655 # End class: MatrixEditDlg
2656 #!!!!!!!!!!!!!!!!!!!!!!!!!#
2657 
2658 
2659 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2660 # Begin class: PredisZeorsDlg
2661 
2662 ##
2663 #
2664 # Dialog to select predistortion zeros.
2665 #
2666 class PredisZeorsDlg(QDialog):
2667 
2668  ##
2669  #
2670  # Create dialog to select visible curves.
2671  # @roots_NRP = Roots of F(s)F(-s) that are in the left-hand s-plane
2672  # @zerosArrang = Predistiortion zeros arrangement (list of bool).
2673  # @param parent = Parent window (default None).
2674  #
2675  def __init__(self, roots_NRP, zerosArrang, parent=None):
2676  super(PredisZeorsDlg, self).__init__(parent)
2677 
2678  ##
2679  # Handle to the dbplot main window.
2680  self.mainWindow = parent
2681 
2682  self.zerosArrang = zerosArrang
2683 
2684  layV = QVBoxLayout(self) # Vertical layout containing everything
2685  title = QLabel("<b>Predistortion synthesis:</b>")
2686  title.setAlignment(Qt.AlignHCenter)
2687  layV.addWidget(title)
2688  layV.addWidget(QLabel("Select zeros of F(s)F(-s) in the"))
2689  layV.addWidget(QLabel("left s-plane that will go to F(s)"))
2690 
2691  layH1 = QHBoxLayout() # Horizontal layout containing grid and stretch
2692  grid = QGridLayout()
2693 
2694  for n in range(self.mainWindow.P.N):
2695  grid.addWidget(QLabel(complexStr(roots_NRP[n], 3)), n, 0, Qt.AlignRight)
2696  checkBox = QCheckBox()
2697  checkBox.setChecked(self.zerosArrang[n])
2698 # self.connect(checkBox, SIGNAL("stateChanged(int)"), lambda value: self.setZeroToFs(n, bool(value)))
2699  self.connect(checkBox, SIGNAL("stateChanged(int)"), partial(self.setZeroToFs, n) )
2700  grid.addWidget(checkBox, n, 1, Qt.AlignHCenter)
2701 
2702  layH1.addLayout(grid)
2703  layH1.addStretch()
2704 
2705  layV.addLayout(layH1)
2706  layV.addStretch()
2707 
2708  #buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
2709  buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
2710  buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
2711  self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
2712  self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
2713 
2714  layV.addWidget(buttonBox)
2715 
2716  self.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
2717  self.setWindowTitle("Zeros F(s)")
2718  self.setToolTip("""Select predistiortion zeros of F(s)F(-s) in the left-hand s-plane that will go to F(s).
2719 If they are not selected, they will go to F(-s).
2720 The zeros of F(s)F(-s) in the right-hand s-plane will go to the opposite polynomial.""")
2721 
2722  def setZeroToFs(self, n, zeroToFs):
2723  self.zerosArrang[n] = bool(zeroToFs)
2724 
2725 # End class: PredisZeorsDlg
2726 #!!!!!!!!!!!!!!!!!!!!!!!!!#
2727 
2728 
2729 #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
2730 # Begin class: SensitivitySPar
2731 
2732 ##
2733 #
2734 # Class for sensitivity analysis.
2735 #
2736 class SensitivityAnalysis(object):
2737 
2738  ##
2739  #
2740  # Constructor: creates SensitivityAnalisys class instance.
2741  # @param parent = Parent widget, in this case is coupling matrix mainWindow.
2742  #
2743  def __init__(self, parent):
2744  ##
2745  # Copy of the coupling matrix mainWindow class instance.
2746  # It is necessary to store a copy since it must be accessed by some member functions.
2747  self.mainWindow = parent
2748 
2749  ##
2750  # QwtPlot that contains the Magnitude and Phase S-parameter plot for all random realizations.
2751  self.SPlotMagPhase = None
2752 
2753  ##
2754  # Numpy array with columns containing S11 for each random realization.
2755  self.S11M = None
2756 
2757  ##
2758  # Numpy array with columns containing S21 for each random realization.
2759  self.S21M = None
2760 
2761  ##
2762  # Numpy array with columns containing S22 for each random realization.
2763  self.S22M = None
2764 
2765  ##
2766  # Numpy array with columns containing phase of S11 for each random realization.
2767  self.phaseS11M = None
2768 
2769  ##
2770  # Numpy array with columns containing phase of S21 for each random realization.
2771  self.phaseS21M = None
2772 
2773  ##
2774  # Numpy array with columns containing phase of S22 for each random realization.
2775  self.phaseS22M = None
2776 
2777  ##
2778  # Numpy array with columns containing groupDelay for each random realization.
2779  self.groupDelayM = None
2780 
2781  ##
2782  # [S] parameters class instance, used to compute the [S] parameters after random variations in coupling matrix.
2783  self.SP = self.mainWindow.CM.SP
2784 
2785 
2786  ##
2787  #
2788  # Run Monte Carlo analysis with random variations in coupling matrix elements.
2789  # Calls self.randomVariations() to obtain a randomly modified coupling matrix with maximum element variations determined by function arguments.
2790  # @param N = Number of Monte Carlo random realizations.
2791  # @param inout = Maximum random variation in % of nominal value, for input/output (source/load) couplings. It is applied to elements in first and last rows and columns.
2792  # @param freq = Maximum random variation in % of nominal value, for resonators resonant frequency.
2793  # @param Q = Maximum random variation in % of nominal value, for resonators quality factor.
2794  # @param inductive = Maximum random variation in % of nominal value, for inductive couplings. It is applied to positive real parts of out-of-diagonal matrix elements.
2795  # @param capacitive = Maximum random variation in % of nominal value, for capacitive couplings. It is applied to negative real parts of out-of-diagonal matrix elements.
2796  # @param resistive = Maximum random variation in % of nominal value, for resistive couplings. It is applied to imaginary parts of out-of-diagonal matrix elements.
2797  #
2798  def runAnalysis(self, N, inout, freq, Q, inductive, capacitive, resistive):
2799  app.setOverrideCursor(Qt.WaitCursor)
2800  MatQ = self.mainWindow.CM.MatQ # Pointer to non-modified CouplingMatrix
2801  # Prepare arrays having the [S] parameters of random realizations in columns
2802  nFreq = len(self.SP.freq)
2803  nFreq2 = nFreq-1 # Number samples for group delay computed from M, groupDelayM
2804  self.S11M, self.S21M, self.S22M = np.zeros((nFreq, N), dtype=np.complex128), np.zeros((nFreq, N), dtype=np.complex128), np.zeros((nFreq, N), dtype=np.complex128)
2805  self.phaseS11M, self.phaseS21M, self.phaseS22M, self.groupDelayM = np.zeros((nFreq, N)), np.zeros((nFreq, N)), np.zeros((nFreq, N)), np.zeros((nFreq2, N))
2806 
2807  maxModReal, maxModImag = np.zeros(MatQ.M.shape), np.zeros(MatQ.M.shape)
2808  maxModQn, maxModF0 = np.zeros(MatQ.Q.shape), np.zeros(MatQ.Q.shape)
2809  first = MatQ.extraNodesS # Index of first resonant node
2810  last = MatQ.M.shape[0] - MatQ.extraNodesL # Index of last resonant node + 1
2811  Qn = MatQ.Q
2812  f0n = MatQ.FT.unormFreq(np.diag(MatQ.M)[first:last].real)
2813 
2814  for n in range(N):
2815  # Copy of current coupling matrix, to which random variations are applied
2816  modMatQ = self.randomVariations(MatQ, inout, inductive, capacitive, resistive, freq, Q )
2817  self.SP.fromCouplingMatrix(modMatQ) # Do not update CM_error here, matrix elements have been modified ramdomly
2818 
2819  # Compute maximum variations, just for verification
2820  maxModReal = np.maximum(maxModReal, 100* np.abs(modMatQ.M.real / MatQ.M.real - 1) )
2821  maxModImag = np.maximum(maxModImag, 100* np.abs(modMatQ.M.imag / MatQ.M.imag - 1) )
2822  modQn = modMatQ.Q
2823  modF0n = MatQ.FT.unormFreq(np.diag(modMatQ.M)[first:last].real)
2824  maxModQn = np.maximum(maxModQn, 100*np.abs(modQn/Qn - 1) )
2825  maxModF0 = np.maximum(maxModF0, 100*np.abs(modF0n/f0n - 1) )
2826 
2827  self.S11M[:, n], self.S21M[:, n], self.S22M[:, n], self.groupDelayM[:, n] = self.SP.S11M, self.SP.S21M, self.SP.S22M, self.SP.groupDelayM
2828  self.phaseS11M[:, n], self.phaseS21M[:, n], self.phaseS22M[:, n] = self.SP.phaseS11M, self.SP.phaseS21M, self.SP.phaseS22M
2829 
2830  # Print maximum variations
2831  # Replace NaN's and Inf's with zero
2832  maxModReal[np.isnan(maxModReal)] = 0
2833  maxModReal[np.isinf(maxModReal)] = 0
2834  maxModImag[np.isnan(maxModImag)] = 0
2835  maxModImag[np.isinf(maxModImag)] = 0
2836 
2837  rowIndices = self.mainWindow.rowIndices
2838  myPrint('\nSENSITIVITY ANALYSIS:')
2839  myPrint('Maximum variation achieved (%), Couplings Real part:')
2840  for m in range(maxModReal.shape[0]):
2841  for n in range(maxModReal.shape[1]):
2842  if n==m: continue
2843  if maxModReal[m, n] > 0: myPrint('(%3s,%3s): %6.3g%%' % (rowIndices[m], rowIndices[n], maxModReal[m, n]))
2844 
2845  myPrint('Maximum variation achieved (%), Couplings Imaginary part:')
2846  for m in range(maxModImag.shape[0]):
2847  for n in range(maxModImag.shape[1]):
2848  if n==m: continue
2849  if maxModImag[m, n] > 0: myPrint('(%3s,%3s): %6.3g%%' % (rowIndices[m], rowIndices[n], maxModImag[m, n]))
2850 
2851  myPrint('Maximum variation achieved (%), Resonators:')
2852  for m in range(maxModQn.shape[0]):
2853  myPrint( 'R%d: fres %6.3g%%, Q %6.3g%%' % (m+1, maxModF0[m], maxModQn[m]) )
2854 
2855  app.restoreOverrideCursor()
2856  self.plotSensitivity()
2857 
2858 
2859  ##
2860  #
2861  # Returns a randomly modified coupling matrix with maximum element variations determined by function arguments. The random variation has uniform distribution.
2862  # @param MatQ = Coupling matrix to which random variations are applied (MatrixQ class instance).
2863  # It is not modified, and the modified (deep) copy is returned.
2864  # @param inout = Maximum random variation in % of nominal value, for input/output (source/load) couplings. It is applied to elements in first and last rows and columns.
2865  # @param inductive = Maximum random variation in % of nominal value, for inductive couplings. It is applied to positive real parts of out-of-diagonal matrix elements \f$ \Re M_{ij} > 0 \qquad i \neq j \f$ .
2866  # @param capacitive = Maximum random variation in % of nominal value, for capacitive couplings. It is applied to negative real parts of out-of-diagonal matrix elements \f$ \Re M_{ij} < 0 \qquad i \neq j \f$ .
2867  # @param resistive = Maximum random variation in % of nominal value, for resistive couplings. It is applied to imaginary parts of out-of-diagonal matrix elements \f$ \Im M_{ij} > 0 \qquad i \neq j \f$ .
2868  # @param f0var = Maximum random variation in % of nominal value, for resonators resonant frequency.
2869  # @param Qvar = Maximum random variation in % of nominal value, for resonators quality factor.
2870  # @return modMatQ = Modified coupling matrix, based on a deep copy of the MatQ argument.
2871  #
2872  # In order to obtain the randomly modified resonant frequency, the real part of diagonal elements corresponding to resonating nodes is unnormalized.
2873  # The random variation is applied to the unnormalized frequency, and it is normalized again to obtain the modified real part of the matrix element.
2874  #
2875  # Diagonal elements corresponding to non-resonant nodes are not modified.
2876  #
2877  # The modified Q of resonators is obtained applying a random variation to the resonator Q of the original matrix (before applying the random variation to resistive couplings).
2878  # Then, the resistive couplings are randomly modified and the new imaginary part of the diagonal element is computed accounting for the new values of the resistive couplings.
2879  #
2880  # In general, for each node \f$ n \f$, the quality factor is
2881  # \f[
2882  # Q_{n} = \frac{1}{G_n \, \mathrm{FBW}}
2883  # \f]
2884  # where the loss conductance of the unloaded resonator is:
2885  # \f[
2886  # G_{n} = - \sum_{m=1}^{N} \Im M_{mn}
2887  # \f]
2888  #
2889  # The losses corresponding to the new \f$ Q'_n \f$ for resonator \f$ n \f$ are:
2890  # \f[
2891  # G'_n = \frac{1}{Q_n' \, \mathrm{FBW}}
2892  # \f]
2893  # But, after the random variation of resistive couplings the losses that we have are
2894  # \f[
2895  # G''_n = - \sum_{m=1}^N \Im M''_{nm}
2896  # \f]
2897  # so, in order to achieve a quality factor \f$ Q' \f$, the modified value of the imaginary part of the diagonal element must be:
2898  # \f[
2899  # \Im M'_{nn} = \Im M''_{nn} + G''_n - G'_n
2900  # \f]
2901  #
2902  def randomVariations(self, MatQ, inout, inductive, capacitive, resistive, f0var, Qvar):
2903  rand = lambda x: 1 + (2*np.random.rand(1)[0]-1)*x/100 # Random number with uniform distribution in [-1-x/100, 1+x/100]
2904 
2905  # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
2906 # eps = MatQ.parent.matrixElementsEps('Sensitivity analysis')
2907  eps = 10** -self.mainWindow.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
2908 
2909  modMatQ = MatQ.copy()
2910  FBW = MatQ.FT.FBW # Fractional bandwidth
2911 
2912  M = modMatQ.M
2913  N = M.shape[0]
2914  first = MatQ.extraNodesS # Index of first resonant node
2915  last = N - MatQ.extraNodesL # Index of last resonant node + 1
2916 
2917  # In the code below, varRe and varIm are the maximum random variation of real and imag parts of M[m,n]
2918  for m in range(N):
2919  for n in range(m+1, N):
2920  #if n==m: continue # Main diagonal will be computed later, after we have computed the variations of resistive couplings
2921  if m==0 or m==N-1 or n==0 or n==N-1: # Input and output couplings (source and load)
2922  if abs(M[m, n]) > eps: varRe, varIm = inout, inout
2923  else: varRe, varIm = 0, 0
2924  else: # Couplings between resonators
2925  if M[m, n].real > eps: varRe = inductive
2926  elif M[m, n].real < -eps: varRe = capacitive
2927  else: varRe, varIm = 0, 0
2928 
2929  if abs(M[m, n].imag) > eps: varIm = resistive
2930  else: varIm = 0
2931 
2932  M[m, n] = complex(M[m, n].real * rand(varRe), M[m, n].imag * rand(varIm))
2933  M[n, m] = M[m, n]
2934 
2935  for n in range(first, last):
2936  # Variation in Q
2937  Qn = MatQ.Q[n-first] # Original Qn (before modifing resisitve couplings)
2938  modQn = Qn * rand(Qvar) # Modified Q after random variation
2939  losses = M[:,n].imag.sum() # Losses, accounting the variations in resistive couplings
2940  modLosses = -1/(FBW * modQn) # Desired losses, according to modified Q
2941  imDiag = MatQ.M[n, n].imag + modLosses - losses # New imaginary part for desired losses
2942 
2943  # Variation in f0
2944  f0n = MatQ.FT.unormFreq(M[n, n].real) # Resonator frequency
2945  modf0n = f0n * rand(f0var)
2946  reDiag = MatQ.FT.normFreq(modf0n)
2947 
2948  M[n, n] = complex(reDiag, imDiag)
2949 
2950  modMatQ.Qresonators()
2951  return modMatQ
2952 
2953 
2954  ##
2955  #
2956  # Plot [S] parameters magnitude and phase graph, if they are available.
2957  # The plotted data is self.S11, self.S21 and self.S22, which contain the [S] parameters for all random realizations, in columns.
2958  #
2959  def plotSensitivity(self):
2960 
2961  SPlotSensitivity = self.mainWindow.mainWindow.SPlotSensitivity
2962 
2963  if SPlotSensitivity is None or SPlotSensitivity.closed is True:
2964 
2965  # Set dynamic range to the lowest value of S21 at sweep range ends.
2966  dynamicRange = -20*log10( min(np.concatenate( (abs(self.S21M[0, :]), abs(self.S21M[-1, :] )) )) )
2967  dynamicRange = 10*ceil(dynamicRange/10)
2968 
2969  SPlotSensitivity = DbPlot( parent=self.mainWindow,
2970  windowTitle = 'Sensitivity analysis results',
2971  tabTitle = '[S] + Group delay',
2972  title = '[S] parameters (from Characteristic Polynomials)',
2973  XData = self.SP.freq / 1e9,
2974  leftYData = [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
2975  rightXData = self.SP.freq2 / 1e9,
2976  rightYData = [ 1e9 * self.groupDelayM ],
2977  leftYmin = -dynamicRange,
2978  leftYmax = 0,
2979  leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
2980  leftYVisible = [True, True, False],
2981  XLabel = 'Frequency (GHz)',
2982  leftYLabel = 'dB',
2983  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'ns',
2984  rightYLabel = 'Time (ns.)' ,
2985  rightYNames = ['Group delay'],
2986  rightYVisible = [True]
2987  )
2988  SPlotSensitivity.addTab( tabTitle = '[S] + Phase',
2989  title = '[S] parameters (from Characteristic Polynomials)',
2990  XData = self.SP.freq / 1e9,
2991  leftYData = [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M)) ],
2992  rightYData = [ self.phaseS21M * 180/np.pi, self.phaseS11M * 180/np.pi, self.phaseS22M * 180/np.pi ],
2993  leftYmin = -dynamicRange,
2994  leftYmax = 0,
2995  leftYNames = ['S<sub>11</sub>', 'S<sub>21</sub>', 'S<sub>22</sub>'],
2996  leftYVisible = [True, True, False],
2997  XLabel = 'Frequency (GHz)',
2998  leftYLabel = 'dB',
2999  Xunits = 'GHz', leftYunits = 'dB', rightYunits = 'deg',
3000  rightYLabel = 'degrees' ,
3001  rightYNames = ['Phase(S<sub>21</sub>)', 'Phase(S<sub>11</sub>)', 'Phase(S<sub>22</sub>)'],
3002  rightYVisible = [True, False, False]
3003  )
3004  SPlotSensitivity.addTab( tabTitle = 'Group delay + Phase',
3005  title = '[S] parameters (from Characteristic Polynomials)',
3006  XData = self.SP.freq2 / 1e9,
3007  leftYData = [1e9 * self.groupDelayM],
3008  rightXData = self.SP.freq / 1e9,
3009  rightYData = [ self.phaseS21M * 180/np.pi ],
3010  leftYNames = ['Group delay'],
3011  XLabel = 'Frequency (GHz)',
3012  leftYLabel = 'Time (ns.)',
3013  Xunits = 'GHz', leftYunits = 'ns', rightYunits = 'deg',
3014  rightYLabel = 'degrees' ,
3015  rightYNames = ['Phase(S<sub>21</sub>)'],
3016  rightYVisible = [True]
3017  )
3018 
3019  self.mainWindow.mainWindow.SPlotSensitivity = SPlotSensitivity
3020 
3021  else:
3022  SPlotSensitivity.update(0, self.SP.freq / 1e9, [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
3023  rightXData = self.SP.freq2 / 1e9, rightYData=[ 1e9 * self.groupDelayM], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3024 
3025  SPlotSensitivity.update(1, self.SP.freq / 1e9, [20*np.log10(np.abs(self.S11M)), 20*np.log10(np.abs(self.S21M)), 20*np.log10(np.abs(self.S22M))],
3026  rightYData=[ self.phaseS21M * 180/np.pi, self.phaseS11M * 180/np.pi, self.phaseS22M * 180/np.pi], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3027 
3028  SPlotSensitivity.update(2, self.SP.freq2 / 1e9, [1e9 * self.groupDelayM], rightXData = self.SP.freq / 1e9, rightYData=[ self.phaseS21M * 180/np.pi ], autoScaleBottomX = self.mainWindow.mainWindow.autoScaleFreq )
3029 
3030  self.mainWindow.mainWindow.autoScaleFreq = False
3031 
3032  # Plot masks, or update if there are any
3033  if self.mainWindow.mainWindow.SM is not None:
3034  SPlotSensitivity.deleteMasks()
3035  self.mainWindow.mainWindow.plotMasks()
3036 
3037 # End class: SensitivityAnalysis
3038 #!!!!!!!!!!!!!!!!!!!!!!!!!#
3039 
3040 
3041 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
3042 # Begin class: CouplingMatrixDlg #
3043 
3044 ##
3045 #
3046 # GUI dialog to show coupling matrices
3047 #
3048 class CouplingMatrixDlg(QMainWindow, Ui_couplingmatrixdlg.Ui_CouplingMatrixDlg):
3049 
3050  ##
3051  #
3052  # Constructor: Creates dialog window with a table to display coupling matrices and buttons to save or store to disk, select matrix from list, plot [S] parameters or edit current matrix.
3053  # @param CM = CouplingMatrix class instance.
3054  # @param parent = Parent widget, in this case is mainWindow.
3055  #
3056  def __init__(self, CM, parent=None):
3057  super(CouplingMatrixDlg, self).__init__(parent)
3058  self.setAttribute(Qt.WA_DeleteOnClose) # Necessary for Modless windows.
3059 
3060  ##
3061  # Copy of the mainWindow class instance.
3062  # It is necessary to store a copy since it must be accessed by some member functions.
3063  self.mainWindow = parent
3064  self.setupUi(self)
3065 
3066  ##
3067  # Matrix edit dialog. It will be a Modeless Live dialog, that is only hidden when closed.
3068  self.matrixEditDlg = None
3069 
3070  ##
3071  # Sensitivity analysis dialog. It will be a Modeless Live dialog, that is only hidden when closed.
3072  self.sensitivityDlg = None
3073 
3074  ##
3075  # Shallow copy of the mainWindow CM class instance, for easier access.
3076  self.CM = CM
3077 
3078  ##
3079  # Attribute that is set to True when CM.MatQ has been modified by user edit actions and not saved in list.
3080  self.dirty = False
3081  self.setWindowModified(self.dirty)
3082 
3083  ##
3084  # Attribute that is set to True when changes to matrix must be recorded.
3085  # It is disabled, for example, when the user does and incorrect matrix edit and the program automatically restores the old matrix value.
3086  self.recordChanges = True
3087 
3088  ##
3089  # Attribute that is set to True before automatic calls to on_action_CM_comboBox_currentIndexChanged() to avoid loading a matrix.
3090  self.noLoadCM = False
3091 
3092  # Add CM selection comboBox to toolBar
3093  self.recordChanges = False # Do not record automatic calls to on_CMcomboBox_currentIndexChanged()
3094  self.CM_comboBox = QComboBox(self.toolBar_2)
3095  self.CM_comboBox.setObjectName("CM_comboBox")
3096  self.CM_comboBox.setToolTip("Select matrix from list")
3097  self.CM_comboBox.setStatusTip("Select matrix from list")
3098  self.connect(self.CM_comboBox, SIGNAL("currentIndexChanged(int)"), self.on_CM_comboBox_currentIndexChanged)
3099  self.toolBar_2.insertWidget(self.action_ListRemove, self.CM_comboBox)
3100 
3101  # Low pass and Q Tab
3102  tabLPQ = QWidget()
3103  self.tabWidget.addTab(tabLPQ, "Low-pass matrix and Q")
3104 
3105  # Horizontal layout that contains a vertical stack with matrix name, matrix table and Q table
3106  self.horizontalLayout_3 = QHBoxLayout(tabLPQ)
3107  self.horizontalLayout_3.addStretch()
3108 
3109  # Vertical layout with matrix name, matrix table and Q table
3110  self.verticalLayout_CM = QVBoxLayout()
3111 
3112  # Matrix name, with horizontal layout
3113  self.horizontalLayout_4 = QHBoxLayout()
3114 
3115  # Create and Matrix Name
3116  self.CM_nameLabel = QLabel('Coupling matrix name:')
3117  self.horizontalLayout_4.addWidget(self.CM_nameLabel)
3118  self.CM_name = QLineEdit('')
3119  self.horizontalLayout_4.addWidget(self.CM_name)
3120  self.horizontalLayout_4.addStretch()
3121  self.CM_prec_label = QLabel('Precision:')
3122  self.horizontalLayout_4.addWidget(self.CM_prec_label)
3123  self.CM_prec = QSpinBox()
3124  self.CM_prec.setRange(3, 14)
3125 
3126  prec = self.mainWindow.CM.matrixElementsEps('Display Coupling Matrix')
3127 # self.CM_prec.setValue(-int ( round(log10(prec)) ))
3128  self.CM_prec.setValue(4) # CE prefers to see less significant digits by default.
3129 
3130  self.connect(self.CM_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3131  self.horizontalLayout_4.addWidget(self.CM_prec)
3132  self.verticalLayout_CM.addLayout(self.horizontalLayout_4)
3133 
3134  # Matrix table
3135  self.CM_tableWidget = myTableWidget(name = "Coupling Matrix", parent = self.centralwidget)
3136  self.CM_tooltip = "To manually edit coupling matrix elements,\nfirst open matrix editor by clicking the toolbar button."
3137  self.CM_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CM_tooltip, None, QApplication.UnicodeUTF8))
3138  self.verticalLayout_CM.addWidget(self.CM_tableWidget)
3139 
3140  # Q table, with horizontal layout
3141  self.horizontalLayout_2 = QHBoxLayout()
3142  self.horizontalLayout_2.addStretch()
3143  self.Q_tableWidget = myTableWidget(name = "Resonators Q", parent = self.centralwidget)
3144  self.Q_tooltip = "To manually edit Q vector elements,\nfirst open matrix editor by clicking the toolbar button."
3145  self.Q_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.Q_tooltip, None, QApplication.UnicodeUTF8))
3146  self.horizontalLayout_2.addWidget(self.Q_tableWidget)
3147  self.Qprec_Layout = QHBoxLayout()
3148  self.Q_prec_label = QLabel('Precision:')
3149  self.Qprec_Layout.addWidget(self.Q_prec_label)
3150  self.Q_prec = QSpinBox()
3151  self.Q_prec.setRange(3, 14)
3152  self.Q_prec.setValue(5)
3153  self.connect(self.Q_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3154  self.Qprec_Layout.addWidget(self.Q_prec)
3155  self.horizontalLayout_2.addLayout(self.Qprec_Layout)
3156  self.horizontalLayout_2.addStretch()
3157  self.verticalLayout_CM.addLayout(self.horizontalLayout_2)
3158 
3159  self.verticalLayout_CM.addStretch()
3160 
3161  self.horizontalLayout_3.addLayout(self.verticalLayout_CM)
3162 
3163  self.horizontalLayout_3.addStretch(1)
3164 
3165  # End Low-pass and Q Tab
3166 
3167  # Band-pass Tab
3168  tabBP = QWidget()
3169  self.tabWidget.addTab(tabBP, "Coupling bandwidth matrix (MHz)")
3170 
3171  # Horizontal layout that contains a vertical stack with matrix name and matrix table
3172  self.horizontalLayout_3bp = QHBoxLayout(tabBP)
3173  self.horizontalLayout_3bp.addStretch()
3174 
3175  # Vertical layout with matrix name, matrix table and Q table
3176  self.verticalLayout_CMbp = QVBoxLayout()
3177 
3178  # Matrix name, with horizontal layout
3179  self.horizontalLayout_4bp = QHBoxLayout()
3180 
3181  # Create and Matrix Name
3182  self.CMbp_nameLabel = QLabel('Coupling matrix name:')
3183  self.horizontalLayout_4bp.addWidget(self.CMbp_nameLabel)
3184  self.CMbp_name = QLabel('')
3185  self.horizontalLayout_4bp.addWidget(self.CMbp_name)
3186  self.horizontalLayout_4bp.addStretch()
3187  self.CMbp_prec_label = QLabel('Precision:')
3188  self.horizontalLayout_4bp.addWidget(self.CMbp_prec_label)
3189  self.CMbp_prec = QSpinBox()
3190  self.CMbp_prec.setRange(3, 14)
3191  self.CMbp_prec.setValue(5)
3192  self.connect(self.CMbp_prec, SIGNAL("valueChanged(int)"), lambda x: self.loadMatrixQ(CM.MatQ) )
3193  self.horizontalLayout_4bp.addWidget(self.CMbp_prec)
3194  self.verticalLayout_CMbp.addLayout(self.horizontalLayout_4bp)
3195 
3196  # Matrix table
3197  self.CMbp_tableWidget = myTableWidget(name = 'Coupling Bandwidth Matrix', parent = self.centralwidget)
3198  self.CMbp_tooltip = "To manually edit coupling matrix elements,\nfirst open matrix editor by clicking the toolbar button."
3199  self.CMbp_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CMbp_tooltip, None, QApplication.UnicodeUTF8))
3200  self.verticalLayout_CMbp.addWidget(self.CMbp_tableWidget)
3201 
3202  # Impedances label
3203  # Only for Jordi Mateu's version of bandpass matrix or bandwidth coupling matrix
3204  # Not relevant in CMSDemo version of bandpass matrix or bandwidth coupling matrix
3205 # self.verticalLayout_CMbp.addWidget(QLabel('Z<sub>0</sub> = Z<sub>S</sub> = Z<sub>L</sub> = 50 Ohm'))
3206 
3207  self.verticalLayout_CMbp.addStretch()
3208 
3209  self.horizontalLayout_3bp.addLayout(self.verticalLayout_CMbp)
3210 
3211  self.horizontalLayout_3bp.addStretch(1)
3212 
3213  # End Band-pass Tab
3214 
3215  # Widget properties
3216  self.CM_name.setReadOnly(True)
3217 
3218  # Format Matrix name as a frameless QLineEdit with bold font and background colour.
3219  self.CM_name.setFrame(False)
3220  font = self.CM_name.font()
3221  font.setBold(True)
3222  self.CM_name.setFont(font)
3223  palette = self.CM_name.palette()
3224  palette.setColor(QPalette.Base, self.palette().window().color())
3225  self.CM_name.setPalette(palette)
3226 
3227  # Set bold font also for band-pass matrix name
3228  self.CMbp_name.setFont(font)
3229 
3230  self.CM_tableWidget.setColumnCount(0)
3231  self.CM_tableWidget.setRowCount(0)
3232  self.Q_tableWidget.setColumnCount(0)
3233  self.Q_tableWidget.setRowCount(0)
3234  self.CMbp_tableWidget.setColumnCount(0)
3235  self.CMbp_tableWidget.setRowCount(0)
3236 
3237  self.CM_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3238  self.Q_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3239  self.CMbp_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3240 
3241  # Layout policy and stretch
3242  self.CM_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3243  self.Q_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3244  self.CMbp_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
3245 # self.CM_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
3246 # self.Q_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
3247 # self.CMbp_tableWidget.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred))
3248 # self.horizontalLayout_3.setStretchFactor(self.CM_tableWidget, 100)
3249 # self.horizontalLayout_2.setStretchFactor(self.Q_tableWidget, 100)
3250 # self.verticalLayout.setStretchFactor(self.CM_tableWidget, 100)
3251 # self.verticalLayout.setStretchFactor(self.Q_tableWidget, 100)
3252 
3253  # Connect table modified signal
3254  self.connect(self.CM_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), self.CM_itemChanged)
3255  self.connect(self.CM_tableWidget, SIGNAL("currentCellChanged(int, int, int, int)"), self.CM_currentCellChanged )
3256  self.connect(self.Q_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), self.Q_itemChanged)
3257  self.connect(self.Q_tableWidget, SIGNAL("currentCellChanged(int, int, int, int)"), self.Q_currentCellChanged )
3258  #self.connect(self.CM_name, SIGNAL("textEdited(QString)"), self.matrixQChanged)
3259  self.connect(self.CM_name, SIGNAL("editingFinished()"), self.matrixQChanged)
3260 
3261  # Add available matrices to CM_comboBox
3262  for MatQ in CM.listM:
3263  self.CM_comboBox.addItem(MatQ.name)
3264 
3265  # Set the current coupling matrix to the previous one
3266  self.CM_comboBox.setCurrentIndex(self.CM.indexCM)
3267 
3268  # Set tables READ-ONLY
3269  self.CM_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3270  self.Q_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3271  self.CMbp_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3272 
3273  # Load coupling matrix and Q tables
3274  self.loadMatrixQ(CM.MatQ)
3275 
3276  self.setWindowModified(False)
3277 
3278  # Start recording changes.
3279  self.recordChanges = True
3280 
3281  # Create matrix edit dialog to allow loading matrices from the coupling matrix window.
3282  self.matrixEditDlg = MatrixEditDlg(self)
3283  self.connect(self.CM_prec, SIGNAL("valueChanged(int)"), self.matrixEditDlg.udpateTopology)
3284 
3285  ##
3286  # SensitivityAnalisys class instance.
3287  self.SA = SensitivityAnalysis(self)
3288 
3289 
3290  ##
3291  #
3292  # Reimplementation of the window close function.
3293  # Sets mainWindow.CMdialog to None.
3294  #
3295  def closeEvent(self, event):
3296  # Check if matrix has been modified and not saved
3297  if not self.okToContinue():
3298  event.ignore()
3299  else:
3300  self.mainWindow.CMdialog = None
3301 
3302  # Close [S] error plot if it exists
3303  if self.mainWindow.SPlotError is not None and not self.mainWindow.SPlotError.closed:
3304  self.mainWindow.SPlotError.close()
3305  self.mainWindow.SPlotError = None
3306 
3307  # Close energy plot if it exists
3308  if self.mainWindow.EnergyPlot is not None and not self.mainWindow.EnergyPlot.closed:
3309  self.mainWindow.EnergyPlot.close()
3310  self.mainWindow.EnergyPlot = None
3311 
3312  # Close sensitivity plot if it exists
3313  if self.mainWindow.SPlotSensitivity is not None and not self.mainWindow.SPlotSensitivity.closed:
3314  self.mainWindow.SPlotSensitivity.close()
3315  self.mainWindow.SPlotSensitivity = None
3316 
3317  event.accept()
3318 
3319 
3320  ##
3321  #
3322  # Load a N+2 coupling matrix into a QTableWidget.
3323  # All actions that modify the coupling matrix, end up executing this method, so the [S] error plot is updated if it exists.
3324  # @param MatQ = MatrixQ class instance.
3325  # @param loadMatrix = Load contents of coupling matrix. Default True.
3326  # @param loadQ = Load contents of Q vector. Default True.
3327  #
3328  def loadMatrixQ(self, MatQ, loadMatrix=True, loadQ=True):
3329 
3330  # Compute Coupling Bandwidth version of current matrix
3331  MatQ.lp2bp2cbw()
3332 
3333  if loadMatrix:
3334 
3335  # Load matrix name
3336  #self.CM_name.setText('<b>' + MatQ.name + '</b>')
3337  self.CM_name.setText(MatQ.name)
3338  self.CMbp_name.setText(MatQ.name)
3339 
3340  # Load low-pass M table
3341  table = self.CM_tableWidget
3342  table.clear()
3343  M = MatQ.M
3344  size = M.shape[0]
3345  first = MatQ.extraNodesS
3346  last = size - MatQ.extraNodesL
3347 
3348  table.clear()
3349  table.setColumnCount(size)
3350  table.setRowCount(size)
3351 
3352  # Row and column header labels
3353  labels = ['S']
3354  for n in range(1, first ): labels.append('NR' + str(n))
3355  labels.extend( [ 'R'+str(elem) for elem in range(1, last-first+1) ] )
3356  for n in range(first, first+MatQ.extraNodesL-1 ): labels.append('NR' + str(n))
3357  labels.append('L')
3358  table.setHorizontalHeaderLabels( labels )
3359  table.setVerticalHeaderLabels( labels )
3360  self.rowIndices = labels # Will be used in matrix rotation pivot indices comboBox
3361  MatQ.rowIndices = labels # Will be used for saving energy and power
3362 
3363  for m in range(size):
3364  for n in range(size):
3365  item = QTableWidgetItem( complexStr(M[m, n], self.CM_prec.value()) )
3366  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3367  table.setItem(m, n, item)
3368 
3369  table.resizeRowsToContents()
3370  table.resizeColumnsToContents()
3371  table.setMaximumSize( table.maximumSizeHint() )
3372 
3373  # Load band-pass M table
3374  table = self.CMbp_tableWidget
3375  table.clear()
3376  Mcbw = MatQ.Mcbw
3377 
3378  table.clear()
3379  table.setColumnCount(size)
3380  table.setRowCount(size)
3381 
3382  # Row and column header labels
3383  table.setHorizontalHeaderLabels( labels )
3384  table.setVerticalHeaderLabels( labels )
3385 
3386  for m in range(size):
3387  for n in range(size):
3388  item = QTableWidgetItem( complexStr(Mcbw[m, n], self.CMbp_prec.value()) )
3389  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3390  table.setItem(m, n, item)
3391 
3392  table.resizeRowsToContents()
3393  table.resizeColumnsToContents()
3394  table.setMaximumSize( table.maximumSizeHint() )
3395 
3396 
3397  # Update matrixEditDlg
3398  if self.matrixEditDlg is not None:
3399  self.matrixEditDlg.setMatrixSize() # Must be called after setting self.rowIndices
3400  self.matrixEditDlg.udpateTopology()
3401  self.matrixEditDlg.Q_doubleSpinBox.setValue( np.mean(np.abs(MatQ.Q)) )
3402 
3403  if loadQ:
3404  # Load Q table
3405  Q = MatQ.Q
3406  N = Q.shape[0] # Number of resonators
3407 
3408  table = self.Q_tableWidget
3409  table.clear()
3410  table.setColumnCount(N)
3411  table.setRowCount(1)
3412  table.setHorizontalHeaderLabels( [ str(elem) for elem in range(1, N+1) ])
3413  table.setVerticalHeaderLabels( [ 'Q' ] )
3414 
3415  for n in range(N):
3416  item = QTableWidgetItem( '%.*g' % (self.Q_prec.value(), Q[n]) )
3417  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3418  table.setItem(0, n, item)
3419 
3420  table.resizeRowsToContents()
3421  table.resizeColumnsToContents()
3422  table.setMaximumSize( table.maximumSizeHint() )
3423 
3424  # Update [S] parameters plot, if it exists
3425  if self.mainWindow.SPlotError is not None and (self.mainWindow.SPlotError.closed is False):
3426  self.on_action_PlotS_triggered(True)
3427 
3428  # Update energy plot, if it exists
3429  if self.mainWindow.EnergyPlot is not None and (self.mainWindow.EnergyPlot.closed is False):
3431 
3432 
3433  ##
3434  #
3435  # This function checks if matrix has been modified and not saved to list. If it has been modified, asks the user to save the matrix.
3436  # The function is executed by the GUI when there is an action that will destroy CM.MatQ: Load matrix from list or read matrix from disk.
3437  # @return ok (True / False) = If True the use has agreed to continue, if False the user cancels the operation.
3438  #
3439  def okToContinue(self):
3440  if self.dirty:
3441  message = "Coupling Matrix has been modified, but not saved to list\nSave?"
3442  reply = QMessageBox.question(self, "%s - Unsaved Changes" % applicationName, message, QMessageBox.Yes | QMessageBox.No| QMessageBox.Cancel)
3443  if reply == QMessageBox.Cancel:
3444  return False
3445  elif reply == QMessageBox.Yes:
3446  self.on_action_ListAdd_triggered(True, programmed=True)
3447  return True
3448 
3449  ##@{
3450  # @name Functions automatically excuted when the user interacts with the GUI
3451 
3452 
3453  ##
3454  #
3455  # Slot that is executed whenever current CM cell is changed
3456  #
3457  def CM_currentCellChanged(self, m, n, mold, nold):
3458  if self.action_Edit.isChecked() is False: return # Do nothing if we not in edit mode
3459 
3460  table = self.CM_tableWidget
3461  item = table.currentItem() # table.item(m, n)
3462  if item is None: return
3463 
3464  self.recordChanges = False
3465  st = complexStr(self.CM.MatQ.M[m, n], 14)
3466  item.setText(st)
3467 
3468  itemOld = table.item(mold, nold)
3469  stOld = complexStr(self.CM.MatQ.M[mold, nold], self.CM_prec.value())
3470  if itemOld is not None: itemOld.setText(stOld)
3471 
3472  self.recordChanges = True
3473 
3474  table.resizeRowsToContents()
3475  table.resizeColumnsToContents()
3476  table.setMaximumSize( table.maximumSizeHint() )
3477 
3478 
3479  ##
3480  #
3481  # Slot that is executed whenever current Q cell is changed
3482  #
3483  def Q_currentCellChanged(self, m, n, mold, nold):
3484  if self.action_Edit.isChecked() is False: return # Do nothing if not in edit mode
3485 
3486  table = self.Q_tableWidget
3487  item = table.currentItem()
3488  if item is None: return
3489 
3490  self.recordChanges = False
3491  st = complexStr(self.CM.MatQ.Q[n], 14)
3492  item.setText(st)
3493 
3494 # # for some reason, itemOld is not the last item, but the one-before-last, but (mold, nold) are correct anyway
3495 # itemOld = table.item(mold, nold)
3496 # stOld = complexStr(self.CM.MatQ.M[mold, nold], self.CM_prec.value())
3497 # if itemOld is not None: itemOld.setText(stOld)
3498 
3499  self.recordChanges = True
3500 
3501  table.resizeRowsToContents()
3502  table.resizeColumnsToContents()
3503  table.setMaximumSize( table.maximumSizeHint() )
3504 
3505 
3506  ##
3507  #
3508  # Slot that is executed whenever CM.MatQ is changed by a matrix edit action.
3509  # The self.dirty attribute is set to True.
3510  #
3511  def matrixQChanged(self):
3512  MatQ = self.CM.MatQ
3513 
3514  if self.sender() is self.CM_name:
3515  st = unicode(self.CM_name.text())
3516  if st != MatQ.name:
3517  MatQ.name = st
3518  MatQ.fileExt = None
3519  self.matrixEditDlg.lastEditAction = 'Matrix name changed to: %s' % st
3520  else: # Nothing has been modified here.
3521  return
3522 
3523  self.dirty = True
3524  self.setWindowModified(self.dirty)
3525 
3526  # Update Q
3527  self.recordChanges = False
3528  MatQ.Qresonators()
3529  self.loadMatrixQ(MatQ, loadMatrix=False)
3530  self.recordChanges = True
3531 
3532  self.pushEditAction()
3533 
3534 
3535  ##
3536  #
3537  # Update the contents of self.CM.MatQ after the user has edited the CM table widget.
3538  #
3539  def CM_itemChanged(self, item):
3540  # Do nothing is recording changes is disabled.
3541  if self.recordChanges == False: return
3542 
3543  row = self.CM_tableWidget.currentRow()
3544  col = self.CM_tableWidget.currentColumn()
3545  M = self.CM.MatQ.M
3546 
3547  if row>=0 and col>=0:
3548  st = str(item.text().trimmed())
3549 
3550  # Python complex() function needs 'j' imaginary unit.
3551  if st.find('i') != -1:
3552  st = st.replace('i','j')
3553  # Update GUI table with complex number using 'j' instead of 'i'.
3554  self.recordChanges = False # Disable recording this matrix change
3555  item.setText(st)
3556  self.recordChanges = True # Re-enable recording.
3557 
3558  try:
3559  value = complex(st)
3560  except ValueError, err:
3561  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect value error. String:<br>%s<br>is not a correct complex number representation.</p>' % (st))
3562 
3563  # Put back the old value in last edited cell
3564  self.recordChanges = False # Disable recording this matrix change
3565  item.setText(complexStr(M[row, col], 4))
3566  self.recordChanges = True # Re-enable recording.
3567 #@todo Set the selected cell back to the cell that was erroneously edited. The following code does not work as it is expected to.
3568 # # Select back last edited cell
3569 # self.CM_tableWidget.setCurrentCell(row, col)
3570 # self.CM_tableWidget.setRangeSelected (QTableWidgetSelectionRange(row, col, row, col), True)
3571 # item.setSelected(True)
3572 # self.CM_tableWidget.setCurrentItem(item)
3573 # self.CM_tableWidget.scrollToItem(item)
3574 
3575  else:
3576  #print "(%d,%d) = %s = (%g,%g)" % (row, col, st, value.real, value.imag)
3577  M[row, col] = value
3578  if row == col:
3579  self.matrixEditDlg.lastEditAction = 'CM Matrix entry edited: (%d,%d) = %s' % (row, col, st)
3580  else: # For non diagonal elements, change also the reciprocal.
3581  M[col, row] = value
3582  itemSym = QTableWidgetItem(st)
3583  itemSym.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3584  self.recordChanges = False # Disable recording this matrix change
3585  self.CM_tableWidget.setItem(col, row, itemSym)
3586  self.recordChanges = True # Re-enable recording.
3587  self.matrixEditDlg.lastEditAction = 'CM Matrix entry edited: (%d,%d) and (%d,%d) = %s' % (row, col, col, row, st)
3588  self.matrixQChanged()
3589 
3590  # Update coupling bandwidth matrix
3591  self.CM.MatQ.lp2bp2cbw()
3592  item = QTableWidgetItem( complexStr(self.CM.MatQ.Mcbw[row, col], self.CMbp_prec.value()) )
3593  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3594  self.CMbp_tableWidget.setItem(row, col, item)
3595  if row != col:
3596  itemSym = QTableWidgetItem( complexStr(self.CM.MatQ.Mcbw[col, row], self.CMbp_prec.value()) )
3597  itemSym.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3598  self.CMbp_tableWidget.setItem(col, row, itemSym)
3599 
3600 
3601  ##
3602  #
3603  # Update the contents of self.CM.MatQ after the user has edited the Q table widget.
3604  #
3605  # For each node \f$ i \f$, the quality factor is
3606  # \f[
3607  # Q_{i} = \frac{1}{G_i \, \mathrm{FBW}}
3608  # \f]
3609  # where the loss conductance of the unloaded resonator is:
3610  # \f[
3611  # G_{i} = - \sum_{j=1}^{N} \Im M_{ji} = - \Im M_{ii} - \sum_{i \neq j} \Im M_{ji}
3612  # \f]
3613  #
3614  # If we change the \f$ Q_i \f$ of a resonator, the new value of \f$ \Im M_{ii} \f$ is:
3615  # \f[
3616  # \Im M_{ii} = - G_{i} - \sum_{i \neq j} \Im M_{ji} = \frac{-1}{Q_{i} \, \mathrm{FBW}} - \sum_{i \neq j} \Im M_{ji}
3617  # \f]
3618  #
3619  def Q_itemChanged(self, item):
3620  # Do nothing is recording changes is disabled.
3621  if self.recordChanges == False: return
3622 
3623  row = self.Q_tableWidget.currentRow()
3624  col = self.Q_tableWidget.currentColumn()
3625  MatQ = self.CM.MatQ
3626  M = MatQ.M
3627  Q = MatQ.Q
3628 
3629  if row>=0 and col>=0:
3630  st = str(item.text().trimmed())
3631 
3632  try:
3633  value = float(st)
3634  except ValueError, err:
3635  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect value error. String:<br>%s<br>is not a correct real number representation.</p>' % (st))
3636 
3637  # Put back the old value in last edited cell
3638  self.recordChanges = False # Disable recording this matrix change
3639  item.setText('%.5g' % Q[col])
3640  self.recordChanges = True # Re-enable recording.
3641  else:
3642  #print "(%d,%d) = %s = %g" % (row, col, st, value)
3643  #oldLosses = -1 / (Q[col] * MatQ.FT.FBW)
3644  #newLosses = -1 / (value * MatQ.FT.FBW)
3645  #deltaM = newLosses - oldLosses
3646  Q[col] = value
3647  self.matrixEditDlg.lastEditAction = 'Q vector entry edited: (%d) = %s' % (col, st)
3648 
3649  # Update diagonal entry of CM matrix
3650  colM = col + MatQ.extraNodesS
3651  losses = -1 / (value * MatQ.FT.FBW)
3652  sumImagColNoDiag = M[:, colM].imag.sum() - M[colM, colM].imag
3653  imDiag = losses - sumImagColNoDiag
3654 
3655  M[colM, colM] = complex(M[colM, colM] .real, imDiag)
3656  itemM = QTableWidgetItem(complexStr(M[colM, colM], 4 ))
3657  itemM.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
3658  self.recordChanges = False # Disable recording this matrix change
3659  self.CM_tableWidget.setItem(colM, colM, itemM)
3660  self.recordChanges = True # Re-enable recording.
3661  self.matrixQChanged()
3662 
3663 
3664  @pyqtSignature("bool")
3665  ##
3666  #
3667  # Run sensitivity analysis
3668  # This function is automatically executed by the GUI when the user pushes the 'Run' button in the sensitivity groupBox.
3669  #
3671  inout = self.inout_doubleSpinBox.value()
3672  inductive = self.inductive_doubleSpinBox.value()
3673  capacitive = self.capacitive_doubleSpinBox.value()
3674  resistive = self.resistive_doubleSpinBox.value()
3675  freq = self.freq_doubleSpinBox.value()
3676  Q = self.Q_doubleSpinBox.value()
3677  N = self.N_spinBox.value()
3678  self.SA.runAnalysis(N, inout, freq, Q, inductive, capacitive, resistive)
3679 
3680 
3681  @pyqtSignature("bool")
3682  ##
3683  #
3684  # Enable or disable matrix edit.
3685  # This function is automatically executed by the GUI when the user toggles the 'Edit' button.
3686  #
3687  def on_action_Edit_toggled(self, checked):
3688  if checked:
3689  self.CM_tableWidget.setEditTriggers(QAbstractItemView.AnyKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
3690  self.CM_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
3691  self.CM_tableWidget.setToolTip("")
3692  self.Q_tableWidget.setEditTriggers(QAbstractItemView.AnyKeyPressed | QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
3693  self.Q_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
3694  self.Q_tableWidget.setToolTip("")
3695  self.CM_name.setReadOnly(False)
3696  self.CM_name.deselect()
3697 
3698  # Format Matrix name as default.
3699  self.CM_name.setFrame(True)
3700  font = self.CM_name.font()
3701  font.setBold(False)
3702  self.CM_name.setFont(font)
3703  palette = self.CM_name.palette()
3704  palette.setColor(QPalette.Base, self.palette().base().color())
3705  self.CM_name.setPalette(palette)
3706 
3707  self.recordChanges = True
3708 
3709  self.matrixEditDlg.show()
3710  else:
3711  self.CM_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3712  self.CM_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3713  self.CM_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.CM_tooltip, None, QApplication.UnicodeUTF8))
3714  self.Q_tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
3715  self.Q_tableWidget.setSelectionMode(QAbstractItemView.NoSelection)
3716  self.Q_tableWidget.setToolTip(QApplication.translate("CouplingMatrixDlg", self.Q_tooltip, None, QApplication.UnicodeUTF8))
3717  self.CM_name.setReadOnly(True)
3718  self.CM_name.deselect()
3719 
3720  # Format Matrix name as a frameless QLineEdit with bold font and background colour.
3721  self.CM_name.setFrame(False)
3722  font = self.CM_name.font()
3723  font.setBold(True)
3724  self.CM_name.setFont(font)
3725  palette = self.CM_name.palette()
3726  palette.setColor(QPalette.Base, self.palette().window().color())
3727  self.CM_name.setPalette(palette)
3728 
3729  self.matrixEditDlg.close()
3730 
3731  # Update the matrix to set the precision after editing matrix entries
3732  self.loadMatrixQ(self.CM.MatQ)
3733 
3734 
3735  @pyqtSignature("bool")
3736  ##
3737  #
3738  # Enable or disable sensitivity analysis.
3739  # This function is automatically executed by the GUI when the user toggles the 'Sensitivity analysis' groupBox.
3740  #
3742  if checked:
3743  pass
3744  else:
3745  pass
3746 
3747 
3748  @pyqtSignature("bool")
3749  ##
3750  #
3751  # Load current matrix from disk.
3752  #
3753  def on_action_Read_triggered(self, checked):
3754  # Check if there are unsaved changes
3755  if not self.okToContinue():
3756  return
3757 
3758  # Set open directory to directory of current file or to lastDir if there is no open file
3759  dir = self.mainWindow.P.outDir
3760 
3761  # Ask for file name
3762  formats = ["*.cm", "*.tcm", "*.fcm", "*.fcm_uniQ", "*.pfi", "*.ina", "*.ins", "*.arrow", "*.cds", "*.triplet", "*.cqs" ]
3763  fileName = unicode(QFileDialog.getOpenFileName(self, "%s - Open Coupling Matrix File" % applicationName, dir, "Coupling matrices files (%s)" % " ".join(formats)))
3764 
3765  # If there is a file to open, read parameters. Otherwise (File Open dialog cancelled by user) do nothing
3766  if fileName:
3767  try:
3768  self.CM.MatQ = self.CM.readCouplingMatrix(fileName)
3769  except parseError, err:
3770  QMessageBox.critical(self, "ERROR", '<p align="center">Error reading coupling matrix file:<br>%s</p>' % err)
3771  return False
3772  self.loadMatrixQ(self.CM.MatQ)
3773  self.matrixEditDlg.lastEditAction = "Matrix '%s' read from disk file '%s'" % (self.CM.MatQ.name, os.path.basename(fileName) )
3774  self.pushEditAction()
3775 
3776 
3777  @pyqtSignature("bool")
3778  ##
3779  #
3780  # Save all matrices to disk, using default file names and extensions.
3781  #
3782  def on_action_SaveAll_triggered(self, checked):
3783  self.CM.saveCouplingMatrices(self.mainWindow.P, self.mainWindow.SP, saveSignificantDigits, saveSignificantDigitsEnergy)
3784 
3785 
3786  @pyqtSignature("bool")
3787  ##
3788  #
3789  # Save current matrix in disk.
3790  #
3791  def on_action_Save_triggered(self, checked):
3792  # Ask for file name
3793  formats = ["*.cm", "*.tcm", "*.fcm", "*.fcm_uniQ", "*.pfi", "*.ina", "*.ins", "*.arrow", "*.cds", "*.triplet", "*.cqs" ]
3794  dir = self.mainWindow.P.outDir
3795  fileName = unicode(QFileDialog.getSaveFileName(self, "%s - Save Coupling Matrix File" % applicationName, dir, "Coupling matrices files (%s)" % " ".join(formats)))
3796 
3797  # If fileName is valid, save parameters. Otherwise (File Save dialog cancelled by user) do nothing
3798  if fileName:
3799  fileBaseName = os.path.basename(fileName)
3800  if "." not in fileBaseName:
3801  fileName += '.cm'
3802  # + self.CM.MatQ.fileExt
3803 
3804  self.CM.MatQ.saveMat(fileName, self.mainWindow.P, self.mainWindow.SP, saveSignificantDigits, saveSignificantDigitsEnergy)
3805  myPrint("Coupling Matrix saved to file: %s" % fileName)
3806 
3807 
3808  @pyqtSignature("bool")
3809  ##
3810  #
3811  # Store current matrix in self.CM_comboBox list.
3812  # @param checked = SIGNAL/SLOT parameter.
3813  # @param programmed = Flag that is true when this function is executed by the program, not the user, to avoid setting CM_comboBox CurrentIndex. Default False.
3814  #
3815  def on_action_ListAdd_triggered(self, checked, programmed=False):
3816  MatQ = self.CM.MatQ
3817  name = MatQ.name
3818  index = self.CM_comboBox.findText(name, Qt.MatchExactly)
3819 
3820  if index >= 0: # name exists in list
3821  message = "Matrix with same name already exists in list\nReplace?"
3822  reply = QMessageBox.question(self, "%s - Matrix exists" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
3823  if reply == QMessageBox.No: # Exit without doing anything
3824  return False
3825  elif reply == QMessageBox.Yes:
3826  self.CM.listM[index] = MatQ.copy()
3827  myPrint("Coupling matrix '%s' replaced in list" % name)
3828  if self.matrixEditDlg is not None and self.recordChanges:
3829  self.matrixEditDlg.lastEditAction = "Matrix '%s' replaced in list" % name
3830  self.pushEditAction()
3831  else: # Append matrix to list
3832  self.CM.listM.append(MatQ.copy())
3833  self.CM_comboBox.addItem(name)
3834  index = self.CM_comboBox.count() - 1
3835  self.CM.indexCM = index
3836  myPrint("Coupling matrix '%s' appended to list" % name)
3837  if self.matrixEditDlg is not None and self.recordChanges:
3838  self.matrixEditDlg.lastEditAction = "Matrix '%s' appended to list" % name
3839  self.pushEditAction()
3840 
3841  self.dirty = False
3842  self.setWindowModified(self.dirty)
3843 
3844  if programmed is False:
3845  # Call setCurrentIndex AFTER setting self.dirty = False, because on_CM_comboBox_currentIndexChanged will be automatically and it calls self.okToContinue() .
3846  self.recordChanges = False
3847  self.CM_comboBox.setCurrentIndex(index)
3848  self.recordChanges = True
3849 
3850 
3851  @pyqtSignature("bool")
3852  ##
3853  #
3854  # Remove current matrix from self.CM_comboBox and CM.listM lists.
3855  #
3857  if self.CM_comboBox.count() == 0:
3858  QMessageBox.warning(self, "WARNING", 'Coupling matrices list is empty' )
3859  return
3860 
3861  index = self.CM_comboBox.currentIndex()
3862  name = str(self.CM_comboBox.currentText())
3863 
3864  message = "Are you sure you what to remove matrix '%s' from list?\nRemove?" % name
3865  reply = QMessageBox.question(self, "%s - Remove matrix" % applicationName, message, QMessageBox.Yes | QMessageBox.No)
3866 
3867  if reply == QMessageBox.Yes:
3868  self.recordChanges = False # Disable changes to avoid CM_comboBox.removeItem re-loading the same matrix
3869  self.CM_comboBox.removeItem(index)
3870  del self.CM.listM[index]
3871  self.recordChanges = True
3872  myPrint("Coupling matrix '%s' removed from list" % name)
3873 
3874  if self.CM_comboBox.count() > 0:
3875  # Load the matrix corresponding to the new index in CM_comboBox
3876  self.on_CM_comboBox_currentIndexChanged(self.CM_comboBox.currentIndex())
3877  self.CM.indexCM = self.CM_comboBox.currentIndex()
3878 
3879 
3880  @pyqtSignature("bool")
3881  ##
3882  #
3883  # Plot stored energy and dissipated power computed from current coupling matrix.
3884  #
3886  SP = self.mainWindow.SP
3887  SP.energyPowerFromCM(self.CM.MatQ)
3888  Nr = np.size(SP.energyRes,1) # Number of resonators
3889 
3890  # Prepare a list of curves for potting
3891  energyCurvesList, energyRevCurvesList, powerCurvesList, powerRevCurvesList, resonatorNames = [], [], [], [], []
3892  for n in range(Nr):
3893  energyCurvesList.append(1e9 * SP.energyRes[:, n])
3894  energyRevCurvesList.append(1e9 * SP.energyResRev[:, n])
3895  powerCurvesList.append(SP.powerRes[:, n])
3896  powerRevCurvesList.append(SP.powerResRev[:, n])
3897  resonatorNames.append('Resonator %d' % (n+1) )
3898 
3899  totalEnergyRes = 1e9 * SP.energyRes.sum(axis=1)
3900  totalEnergyResRev = 1e9 * SP.energyResRev.sum(axis=1)
3901  totalPowerRes = SP.powerRes.sum(axis=1)
3902  totalPowerResRev = SP.powerResRev.sum(axis=1)
3903  energyCurvesList.append(totalEnergyRes)
3904  energyRevCurvesList.append(totalEnergyResRev)
3905  powerCurvesList.append(totalPowerRes)
3906  powerRevCurvesList.append(totalPowerResRev)
3907  resonatorNames.append('Total')
3908 
3909  # Threshold to consider the real or imaginary parts of a matrix element positive, negative or non-zero
3910  eps = 10** -self.CM_prec.value() # Keep matrix topology in sync which what is displayed in the matrix table
3911 
3912  powerCoupCurves, powerCoupRevCurves = [], []
3913  powerCoupNames = []
3914  powerCoupTotal = np.zeros(SP.freq.shape)
3915  powerCoupRevTotal = 0
3916  for coup in SP.powerCoup:
3917  if np.abs(coup[1]) < eps: continue # Filter couplings with Gmn smaller than matrix precision
3918  (m, n) = coup[0]
3919  powerCoupTotal += coup[2]
3920  powerCoupRevTotal += coup[3]
3921  powerCoupCurves.append(coup[2])
3922  powerCoupRevCurves.append(coup[3])
3923  powerCoupNames.append('(%s,%s) = %.3g' % (self.rowIndices[m], self.rowIndices[n], coup[1] ))
3924 
3925  powerCoupCurves.append(powerCoupTotal)
3926  powerCoupRevCurves.append(powerCoupRevTotal)
3927  powerCoupNames.append('Total')
3928 
3929 # powerCoupCurves.append(totalPowerRes)
3930 # powerCoupRevCurves.append(totalPowerResRev)
3931 # powerCoupNames.append('Total at resonators')
3932 
3933 # energyCoupCurves, energyCoupRevCurves = [], []
3934 # energyCoupNames = []
3935 # for coup in SP.energyCoup:
3936 # if np.abs(coup[1]) < eps: continue # Filter couplings with Bmn smaller than matrix precision
3937 # (m, n) = coup[0]
3938 # energyCoupCurves.append(1e9*coup[2])
3939 # energyCoupRevCurves.append(1e9*coup[3])
3940 # energyCoupNames.append('(%s,%s) = %.3g' % (self.rowIndices[m], self.rowIndices[n], coup[1] ))
3941 
3942  # Check if an EnergyPlot plot exists to create a new one or to replace existing data.
3943  if self.mainWindow.EnergyPlot is None or self.mainWindow.EnergyPlot.closed:
3944  # Now that there is no open EnergyPlot window, create a new one.
3945  self.mainWindow.EnergyPlot = EnergyDbPlot(parent=self,
3946  windowTitle = 'Stored Energy and dissipated power',
3947  tabTitle = 'Energy',
3948  title = 'Stored energy in the resonators computed from CM',
3949  XData = SP.freq / 1e9,
3950  leftYData = energyCurvesList,
3951  leftYNames = resonatorNames,
3952  XLabel = 'Frequency (GHz)',
3953  leftYLabel = 'nJ',
3954  Xunits = 'GHz', leftYunits = 'nJ',
3955  )
3956  self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = energyRevCurvesList, rightYDataRev = None)
3957 
3958  self.mainWindow.EnergyPlot.addTab(tabTitle = 'Power',
3959  title = 'Dissipated power in the resonators computed from CM',
3960  XData = SP.freq / 1e9,
3961  leftYData = powerCurvesList,
3962  leftYNames = resonatorNames,
3963  XLabel = 'Frequency (GHz)',
3964  leftYLabel = 'W',
3965  Xunits = 'GHz', leftYunits = 'W',
3966  )
3967  self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = powerRevCurvesList, rightYDataRev = None)
3968 
3969  self.mainWindow.EnergyPlot.addTab( tabTitle = 'Couplings power',
3970  title = 'Dissipated power in the couplings computed from CM',
3971  XData = SP.freq / 1e9,
3972  leftYData = powerCoupCurves,
3973  leftYNames = powerCoupNames,
3974  XLabel = 'Frequency (GHz)',
3975  leftYLabel = 'W',
3976  Xunits = 'GHz', leftYunits = 'W',
3977  )
3978  self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = powerCoupRevCurves, rightYDataRev = None)
3979 
3980  else:
3981  self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3982  leftYData = energyCurvesList,
3983  leftYNames = resonatorNames,
3984  replaceTab = 0 )
3985  self.mainWindow.EnergyPlot.replaceReverseData(0, leftYDataRev = energyRevCurvesList, rightYDataRev = None)
3986 
3987  self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3988  leftYData = powerCurvesList,
3989  leftYNames = resonatorNames,
3990  replaceTab = 1 )
3991  self.mainWindow.EnergyPlot.replaceReverseData(1, leftYDataRev = powerRevCurvesList, rightYDataRev = None)
3992 
3993  self.mainWindow.EnergyPlot.addTab( XData = SP.freq / 1e9,
3994  leftYData = powerCoupCurves,
3995  leftYNames = powerCoupNames,
3996  replaceTab = 2 )
3997  self.mainWindow.EnergyPlot.replaceReverseData(2, leftYDataRev = powerCoupRevCurves, rightYDataRev = None)
3998 
3999 
4000 # self.mainWindow.EnergyPlot.addTab( tabTitle = 'Couplings energy',
4001 # title = 'Stored energy (W<sub>m</sub> - W<sub>e</sub>) at magnetic or electric couplings computed from CM',
4002 # XData = SP.freq / 1e9,
4003 # leftYData = energyCoupCurves,
4004 # leftYNames = energyCoupNames,
4005 # XLabel = 'Frequency (GHz)',
4006 # leftYLabel = 'nJ',
4007 # Xunits = 'GHz', leftYunits = 'nJ',
4008 # )
4009 # self.mainWindow.EnergyPlot.appendReverseData(leftYDataRev = energyCoupRevCurves, rightYDataRev = None)
4010 
4011 
4012  @pyqtSignature("bool")
4013  ##
4014  #
4015  # Plot [S] parameters computed from current coupling matrix.
4016  # S21 and S11 are plotted on top of [S] parameters computed from Characteristic Polynomials.
4017  #
4018  def on_action_PlotS_triggered(self, checked):
4019  SP = self.mainWindow.SP
4020 
4021  # Compute [S] parameters from current coupling matrix
4022  myPrint('Displayed coupling matrix:')
4023  stErrorCM, CM_error = SP.fromCouplingMatrix(self.CM.MatQ) # Do not update CM.CM_error with these results, since the matrix may have been edited by the user.
4024 
4025  # Check if an [S] parameter plot exists and, if not, create a new one with [S] computed from CPs
4026  if self.mainWindow.SPlotError is None or self.mainWindow.SPlotError.closed is True:
4027  # Set dynamic range to the lowest value of S21 at sweep range ends.
4028  dynamicRange = -20*log10( min([ abs(SP.S21[0]), abs(SP.S21[-1] ), abs(SP.S21M[0]), abs(SP.S21M[-1] )]))
4029  dynamicRange = 10*ceil(dynamicRange/10)
4030 
4031  self.mainWindow.SPlotError = DbPlot(parent=self.mainWindow,
4032  windowTitle = '[S] parameters CM vs CP',
4033  tabTitle = 'S11',
4034  title = 'S11 parameter computed from CM and CP',
4035  XData = SP.freq / 1e9,
4036  leftYData = [20*np.log10(np.abs(SP.S11)), 20*np.log10(np.abs(SP.S11M)), 20*np.log10(np.abs(SP.S22)), 20*np.log10(np.abs(SP.S22M)) ],
4037  leftWidth = [2, 1, 2, 1],
4038  leftYmax = 0, leftYmin = -dynamicRange,
4039  leftYNames = ['S<sub>11</sub> from CP', 'S<sub>11</sub> from CM', 'S<sub>22</sub> from CP', 'S<sub>22</sub> from CM'] ,
4040  leftYVisible = [True, True, False, False],
4041  XLabel = 'Frequency (GHz)',
4042  leftYLabel = 'dB',
4043  Xunits = 'GHz', leftYunits = 'dB',
4044  rightYData = [ SP.phaseS11 * 180/np.pi, SP.phaseS11M * 180/np.pi, SP.phaseS22 * 180/np.pi, SP.phaseS22M * 180/np.pi ],
4045  rightWidth = [2, 1, 2, 1],
4046  rightYunits = 'deg', rightYLabel = 'degrees' ,
4047  rightYNames = ['Phase(S<sub>11</sub>) from CP', 'Phase(S<sub>11</sub>) from CM', 'Phase(S<sub>22</sub>) from CP', 'Phase(S<sub>22</sub>) from CM'],
4048  rightYVisible = [False, False, False, False],
4049  textLabel =stErrorCM
4050  )
4051 
4052  self.mainWindow.SPlotError.addTab(tabTitle = 'S21',
4053  title = 'S21 parameter computed from CM and CP',
4054  XData = SP.freq / 1e9,
4055  leftYData = [20*np.log10(np.abs(SP.S21)), 20*np.log10(np.abs(SP.S21M)) ],
4056  leftWidth = [2, 1] ,
4057  leftYmax = 0, leftYmin = -dynamicRange,
4058  leftYNames = ['S<sub>21</sub> from CP', 'S<sub>21</sub> from CM'] ,
4059  XLabel = 'Frequency (GHz)',
4060  leftYLabel = 'dB',
4061  Xunits = 'GHz', leftYunits = 'dB',
4062  rightYData = [ SP.phaseS21 * 180/np.pi, SP.phaseS21M * 180/np.pi ],
4063  rightWidth = [2, 1],
4064  rightYunits = 'deg', rightYLabel = 'degrees' ,
4065  rightYNames = ['Phase(S<sub>21</sub>) from CP', 'Phase(S<sub>21</sub>) from CM'],
4066  rightYVisible = [False, False],
4067  )
4068 
4069  self.mainWindow.SPlotError.addTab(tabTitle = 'Group delay',
4070  title = 'Group delay computed from CM and CP',
4071  XData = SP.freq2 / 1e9,
4072  leftYData = [ 1e9 * SP.groupDelay2, 1e9 * SP.groupDelayM ],
4073  leftWidth = [2, 1] ,
4074  leftYNames = ['Group delay from CP', 'Group delay from CM'],
4075  XLabel = 'Frequency (GHz)',
4076  leftYLabel = 'Group delay (ns.)',
4077  Xunits = 'GHz', leftYunits = 'ns',
4078  )
4079 
4080  else:
4081  self.mainWindow.SPlotError.update(0, XData = SP.freq / 1e9, leftYData = [20*np.log10(np.abs(SP.S11)), 20*np.log10(np.abs(SP.S11M)), 20*np.log10(np.abs(SP.S22)), 20*np.log10(np.abs(SP.S22M))],
4082  rightYData = [ SP.phaseS11 * 180/np.pi, SP.phaseS11M * 180/np.pi, SP.phaseS22 * 180/np.pi, SP.phaseS22M * 180/np.pi ],
4083  textLabel =stErrorCM, autoScaleBottomX = self.mainWindow.autoScaleFreq )
4084 
4085  self.mainWindow.SPlotError.update(1, XData = SP.freq / 1e9, leftYData = [20*np.log10(np.abs(SP.S21)), 20*np.log10(np.abs(SP.S21M))],
4086  rightYData = [ SP.phaseS21 * 180/np.pi, SP.phaseS21M * 180/np.pi ], autoScaleBottomX = self.mainWindow.autoScaleFreq)
4087 
4088  self.mainWindow.SPlotError.update(2, XData = SP.freq2 / 1e9, leftYData = [1e9 * SP.groupDelay2, 1e9 * SP.groupDelayM], autoScaleBottomX = self.mainWindow.autoScaleFreq )
4089 
4090  self.mainWindow.autoScaleFreq = False
4091 
4092 
4093  # Plot masks, or update if there are any
4094  if self.mainWindow.SM is not None:
4095  self.mainWindow.SPlotError.deleteMasks()
4096  self.mainWindow.plotMasks()
4097 
4098 
4099  @pyqtSignature("int")
4100  ##
4101  #
4102  # Load a new coupling matrix into the QTableWidget.
4103  # This function is automatically executed by the GUI when the user changes the coupling matrix type comboBox.
4104  #
4106  if self.noLoadCM: return
4107 
4108  # Check if matrix has been modified and not saved
4109  if not self.okToContinue(): # self.recordChanges condition is necessary to avoid recursion
4110  QMessageBox.warning(self, "WARNING", 'User cancelled action.<p>Coupling Matrix has not been loaded from list.' )
4111  # Restore last index to comboBox
4112  self.noLoadCM = True
4113  self.CM_comboBox.setCurrentIndex(self.CM.indexCM) # Will can on_CM_comboBox_currentIndexChanged() recursively
4114  self.noLoadCM = False
4115  return
4116 
4117  MatQ = self.CM.listM[index].copy() # Deep copy: we do not want to modify the precomputed matrices
4118  self.CM.MatQ = MatQ
4119  self.loadMatrixQ(MatQ)
4120  if self.recordChanges:
4121  self.CM.indexCM = index
4122 
4123  self.dirty = False
4124  self.setWindowModified(self.dirty)
4125 
4126  # After the matrix editor has been opened for the first time, loading a matrix from the list is an edit action than modifies CM.MatQ, so it must be recorded in the changes lists.
4127  if self.matrixEditDlg is not None and self.recordChanges:
4128  self.matrixEditDlg.lastEditAction = "Matrix %s loaded from list" % MatQ.name
4129  self.pushEditAction()
4130 
4131 
4132  def pushEditAction(self):
4133  self.matrixEditDlg.edit_listWidget.addItem(self.matrixEditDlg.lastEditAction)
4134  self.matrixEditDlg.bkpActions.append(self.matrixEditDlg.lastEditAction)
4135  self.matrixEditDlg.edit_listWidget.setCurrentRow(self.matrixEditDlg.edit_listWidget.count()-1) # Set edit_listWidget current row to last one
4136  myPrint("Done: Matrix edit action '%s'" % self.matrixEditDlg.lastEditAction)
4137  self.setWindowModified(self.dirty)
4138  self.CM_name.deselect()
4139 
4140  # Push modified CM.MatQ to backup list. Must use deep copy, since CM.MatQ will be modified by edit actions.
4141  self.matrixEditDlg.bkpMatQ.append(self.CM.MatQ.copy())
4142 
4143  ##@}
4144 
4145 # End class: CouplingMatrixDlg #
4146 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
4147 
4148 #¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡#
4149 # Begin class: ParamEditDlg #
4150 
4151 ##
4152 #
4153 # GUI dialog to edit synthesis parameters
4154 #
4155 class ParamEditDlg(QMainWindow, Ui_parameditdlg.Ui_ParamEditDlg):
4156 
4157  ##
4158  #
4159  # Constructor: Creates dialog window containing values of parameters.
4160  # Modifies MainWindow.P , using a shallow copy for easier access.
4161  # Saves a backup copy of the mainWindow Parameters and synthesis results, that will be restored if the user Discards the dialog.
4162  # @param parent = Parent widget, in this case is mainWindow.
4163  #
4164  def __init__(self, parent=None):
4165  super(ParamEditDlg, self).__init__(parent)
4166  self.setAttribute(Qt.WA_DeleteOnClose)
4167 
4168  ##
4169  # Index of the Center Frequency Units comboBox.
4170  # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4171  self.f0UnitsIndex = None
4172 
4173  ##
4174  # Index of the Bandwidth Units comboBox.
4175  # It is used to check if the user has changed the Bandwidth Units in the GUI and to update the Bandwidth value accordingly.
4176  self.BWUnitsIndex = None
4177 
4178  ##
4179  # Index of the Quasieliptic zero frequency units comboBox.
4180  # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4181  self.qeZeroUnitsIndex = None
4182 
4183  ##
4184  # Index of the Generalized Chebyshev zeros frequency units comboBox.
4185  # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4186  self.transmissionZerosUnitsIndex = None
4187 
4188  ##
4189  # Index of the Minimum Frequency Units comboBox.
4190  # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4191  self.minFreqUnitsIndex = None
4192 
4193  ##
4194  # Index of the Maximum Frequency Units comboBox.
4195  # It is used to check if the user has changed the Frequency Units in the GUI and to update the Frequency value accordingly.
4196  self.maxFreqUnitsIndex = None
4197 
4198  ##
4199  # Variable that becomes true when the user clicks the 'Discard' button to reject the dialog.
4200  # Its is necessary to use this variable, since now the dialog is a QMainWindow instead of a QDialog, and QMainWindow does not have a reject() slot.
4201  self.rejected = False
4202 
4203  ##
4204  # Variable that is False when it is necessary to recompute synthesis because Parameters have been modified.
4205  self.recomputed = True
4206 
4207  ##
4208  # Variable to store a list of widgets with bad input.
4209  # Only when this list is empty, he "Accept" and "Compute" buttons are enabled.
4210  self.badInput = [ ]
4211 
4212  ##
4213  # Variable to store sweep value in order to check if it has been changed by the user, and then update frequency axis. Tuple (fmin, fmax)
4214  oldSweep = None
4215 
4216  ##
4217  # Copy of the mainWindow class instance.
4218  # It is necessary to store a copy since it must be accessed by some member functions.
4219  self.mainWindow = parent
4220  self.setupUi(self)
4221 
4222 # if importedExtraFilters:
4223 # self.topol_comboBox.removeItem(self.topol_comboBox.findText(u'Quasieliptic'))
4224 # self.filter_stackedWidget.removeWidget(self.filterQuasieliptic_page)
4225 
4226  ##
4227  # Shallow copy just for easier access.
4228  self.P = self.mainWindow.P
4229 
4230  ##
4231  # Backup copy of the mainWindow Parameters and synthesis results, that will be restored if the user Discards the dialog.
4232  self.resultBkp = copy.deepcopy( (self.mainWindow.P, self.mainWindow.CP, self.mainWindow.CM, self.mainWindow.SP, self.mainWindow.FT) )
4233 
4234  ##
4235  # Initial dialog size as set in Qt designer
4236  self.dlgInitSize = self.size().height()
4237 
4238  # Set active tab to 'Filter parameters'
4239  self.tabWidget.setCurrentIndex(0)
4240 
4241  # Connect self.N_spinBox to a method that updates pre-distortion zeros arrangement.
4242  self.connect(self.N_spinBox, SIGNAL("valueChanged(int)"), self.updatePredisZerosArrang )
4243 
4244  # Create adaptive predistortion weights table
4245  self.wFunc_horizontalLayout = QHBoxLayout()
4246  self.wFunc_horizontalLayout.addStretch()
4247  self.wFunc_tableWidget = myTableWidget(name = 'Adaptive predistortion weights', parent = self.adapPredis_groupBox)
4248  self.wFunc_tooltip = "Adaptive predistortion weights.\nThe shift is equal to the weight times sigma:\nweights = 1 lead to conventional pole shifting.\nTable entries are editable."
4249  self.wFunc_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.wFunc_tooltip, None, QApplication.UnicodeUTF8))
4250  self.wFunc_tableWidget.horizontalHeader().setVisible(False)
4251  self.wFunc_horizontalLayout.addWidget(self.wFunc_tableWidget)
4252  self.wFunc_horizontalLayout.addStretch()
4253  self.wFunc_verticalLayout.addLayout(self.wFunc_horizontalLayout)
4254  self.wFunc_verticalLayout.setAlignment(Qt.AlignHCenter)
4255  self.connect(self.N_spinBox, SIGNAL("valueChanged(int)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), True ) )
4256  self.connect(self.adapPredis_groupBox, SIGNAL("toggled(bool)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), True ) )
4257  self.connect(self.wFunc_tableWidget, SIGNAL("itemChanged(QTableWidgetItem*)"), lambda x: self.updatewFuncTableSize(self.P.wFunc, (self.N_spinBox.value(), 1), False ))
4258 
4259  # Create GenCheby transmission zeros List
4260  self.genChebyTZs_horizontalLayout = QHBoxLayout()
4261  self.genChebyTZs_horizontalLayout.addStretch()
4262  self.genChebyTZs_tableWidget = myTableWidget(name = 'Transmission zeros', mainDlg = self, parent = self.genChebyLP_groupBox)
4263  self.genChebyTZs_tableWidget.setUnits('MHz')
4264  self.genChebyTZs_tooltip = "Transmission zeros (complex or purely imaginary). \nReal part in normalized frequency units and imaginary part in MHz. Imaginary part cannot be 0 MHz. \nFor complex zeros, a symmetric zero with opposite real part is automatically added."
4265  self.genChebyTZs_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.genChebyTZs_tooltip, None, QApplication.UnicodeUTF8))
4266  self.genChebyTZs_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
4267  self.genChebyTZs_horizontalLayout.addWidget(self.genChebyTZs_tableWidget)
4268  self.genChebyTZs_horizontalLayout.addStretch()
4269  self.genCheby_verticalLayout.insertLayout(2, self.genChebyTZs_horizontalLayout)
4270  self.genCheby_verticalLayout.setAlignment(Qt.AlignHCenter)
4271 
4272  self.connect(self.genChebyTZs_tableWidget, SIGNAL("cellChanged(int, int)"), self.genChebyTZs_tableWidget.tableCellChanged)
4273 
4274  # Create GenCheby LP complex zeros List
4275  self.LPcomplexZeros_horizontalLayout = QHBoxLayout()
4276  self.LPcomplexZeros_horizontalLayout.addStretch()
4277  self.LPcomplexZeros_tableWidget = myTableWidget(name = 'Initial positions of complex zeros to optimize', mainDlg = self, parent = self.genChebyLP_groupBox)
4278  self.LPcomplexZeros_tableWidget.setUnits('MHz')
4279  self.LPcomplexZeros_tooltip = "Initial positions of complex zeros to optimize for linear phase equalization. \nReal part in normalized frequency units and imaginary part in MHz. \nImaginary part of a complex zero cannot be 0 MHz. \nIf imaginary part is empty, the zero is purely real.\nA symmetric zero with opposite real part is automatically added."
4280  self.LPcomplexZeros_tableWidget.setToolTip(QApplication.translate("ParamEditDlg", self.LPcomplexZeros_tooltip, None, QApplication.UnicodeUTF8))
4281  self.LPcomplexZeros_tableWidget.setSelectionMode(QAbstractItemView.SingleSelection)
4282  self.LPcomplexZeros_horizontalLayout.addWidget(self.LPcomplexZeros_tableWidget)
4283  self.LPcomplexZeros_horizontalLayout.addStretch()
4284  self.genChebyLPverticalLayout.insertLayout(4, self.LPcomplexZeros_horizontalLayout)
4285  self.genChebyLPverticalLayout.setAlignment(Qt.AlignHCenter)
4286 
4287  self.connect(self.LPcomplexZeros_tableWidget, SIGNAL("cellChanged(int, int)"), self.LPcomplexZeros_tableWidget.tableCellChanged)
4288 
4289  # Update TZs tables when changing f0
4290  self.connect(self.f0_lineEdit, SIGNAL("editingFinished()"), self.genChebyTZs_tableWidget.setf0 )
4291  self.connect(self.f0_lineEdit, SIGNAL("editingFinished()"), self.LPcomplexZeros_tableWidget.setf0 )
4292 
4293  # Load parameters into GUI widgets
4294  self.updateDialogParams()
4295 
4296  # Create validators. Validator regular expressions are global variables.
4297  #realValidator = QDoubleValidator(self)
4298  realValidator = QRegExpValidator( reReal , self)
4299  #realPositiveValidator = QDoubleValidator(self)
4300  #realPositiveValidator.setBottom(0)
4301  realPositiveValidator = QRegExpValidator( reRealPositive, self)
4302  realPositiveValidatorInf = QRegExpValidator( reRealPositiveInf, self)
4303  complexValidator = QRegExpValidator( reComplex, self)
4304  realListValidator = QRegExpValidator( reRealList, self)
4305  realPositiveListValidator = QRegExpValidator( reRealPositiveList, self)
4306  complexListValidator = QRegExpValidator( reComplexList, self)
4307 
4308  # Set validators
4309  self.f0_lineEdit.setValidator(realPositiveValidator)
4310  self.BW_lineEdit.setValidator(realPositiveValidator)
4311  self.maxFreq_lineEdit.setValidator(realPositiveValidator)
4312  self.minFreq_lineEdit.setValidator(realPositiveValidator)
4313  self.chebyLr_lineEdit.setValidator(realPositiveValidator)
4314  self.qeLr_lineEdit.setValidator(realPositiveValidator)
4315  self.qeZero_lineEdit.setValidator(realPositiveValidator)
4316  self.genChebyLr_lineEdit.setValidator(realPositiveValidator)
4317 # self.transmissionZeros_lineEdit.setValidator(realPositiveListValidator)
4318 # self.LPcomplexZeros_lineEdit.setValidator(complexListValidator)
4319 # self.LPfracBW_lineEdit.setValidator(realPositiveValidator) # Now this widget is a doubleSpinBox
4320 # self.LPripple_lineEdit.setValidator(realPositiveValidator) # Now this widget is a doubleSpinBox
4321  self.Qo_lineEdit.setValidator(realPositiveValidatorInf)
4322  self.QoPredis_lineEdit.setValidator(realPositiveValidatorInf)
4323  self.Qef_lineEdit.setValidator(realPositiveValidatorInf)
4324 # self.wFunc_lineEdit.setValidator(realListValidator)
4325 
4326  self.nuqK11c1_lineEdit.setValidator(realPositiveValidator)
4327  self.nuqK22c1_lineEdit.setValidator(realPositiveValidator)
4328  self.nuqK21c1_lineEdit.setValidator(realPositiveValidator)
4329  self.nuqK21c2_lineEdit.setValidator(realPositiveValidator)
4330  self.nuqK21c3_lineEdit.setValidator(realPositiveValidator)
4331 
4332  self.nuqDelta_lineEdit.setValidator(realPositiveValidator)
4333 
4334  ##
4335  # Variable to store the modified/unmodifed status of the dialog working copy of parameters self.P.
4336  # The dialogChanged() method is automatically called when the user modifies the edit widgets contents and sets this variable to True.
4337  # This variable must be set to False at the end of the __init__ dialog constructor,
4338  # since dialogChanged() is called when the constructor sets the edit widgets contents.
4339  self.dialogModified = False
4340 
4341 
4342  ##
4343  #
4344  # Updates paramEditDlg widgets with contents of local copy of P (self.P).
4345  # @param autoFreqUnits = If True, automatically update frequency units in ComboBoxes. It is True by default.
4346  #
4347  def updateDialogParams(self, autoFreqUnits = True):
4348 
4349  # Local copy of parameters
4350  P = self.P
4351 
4352  # Set output file and directory names
4353  if self.P.outName is not None: self.outName_lineEdit.setText(self.P.outName)
4354  if self.P.outDir is not None: self.outDir_lineEdit.setText(self.P.outDir)
4355 
4356  # Set Synthesis parameters widgets
4357  if self.P.N is not None: self.N_spinBox.setValue(self.P.N)
4358 
4359  if self.P.f0 is not None:
4360  (freq, units) = self.frequencyUnits(self.P.f0)
4361  autoUnitsIndex = self.f0_comboBox.findText(units)
4362 
4363  if autoFreqUnits: # Set automatic frequency units
4364  self.f0_lineEdit.setText(str(freq))
4365  self.f0_comboBox.setCurrentIndex(autoUnitsIndex)
4366  self.f0UnitsIndex = autoUnitsIndex
4367  else: # Keep user frequency units
4368  userUnitsIndex = self.f0_comboBox.currentIndex()
4369  newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4370  self.f0_lineEdit.setText(str(newValue))
4371  self.f0UnitsIndex = userUnitsIndex
4372  else:
4373  self.f0UnitsIndex = self.f0_comboBox.currentIndex()
4374 
4375  # Update f0 in TZs tables, for computation of f-f0 in 3rd column
4376  self.genChebyTZs_tableWidget.setf0(False)
4377  self.LPcomplexZeros_tableWidget.setf0(False)
4378 
4379  if self.P.BW is not None:
4380  (freq, units) = self.frequencyUnits(self.P.BW)
4381  autoUnitsIndex = self.BW_comboBox.findText(units)
4382 
4383  if autoFreqUnits: # Set automatic frequency units
4384  self.BW_lineEdit.setText(str(freq))
4385  self.BW_comboBox.setCurrentIndex(autoUnitsIndex)
4386  self.BWUnitsIndex = autoUnitsIndex
4387  else: # Keep user frequency units
4388  userUnitsIndex = self.BW_comboBox.currentIndex()
4389  newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4390  self.BW_lineEdit.setText(str(newValue))
4391  self.BWUnitsIndex = userUnitsIndex
4392  else:
4393  self.BWUnitsIndex = self.BW_comboBox.currentIndex()
4394 
4395  if self.P.filterTransferFunc is not None:
4396  topol = self.P.filterTransferFunc.title() # Capital initials for safe comparison with GUI ComboBox items
4397  iTopol = self.topol_comboBox.findText(topol)
4398  else: iTopol = 0
4399 
4400  if iTopol < 0:
4401  QMessageBox.warning(self, "WARNING", "Unknown Filter Topology '%s' replaced by default value." % self.P.filterTransferFunc)
4402  iTopol = 0
4403  self.topol_comboBox.setCurrentIndex(iTopol)
4404  self.filter_stackedWidget.setCurrentIndex(iTopol)
4405 
4406  # Set filter topology widgets
4407  if self.P.chebyLr is not None: self.chebyLr_lineEdit.setText(str(self.P.chebyLr))
4408  if self.P.qeLr is not None: self.qeLr_lineEdit.setText(str(self.P.qeLr))
4409 
4410  #if self.P.qeZero is not None: self.qeZero_lineEdit.setText(str(self.P.qeZero))
4411  if self.P.qeZero is not None:
4412  (freq, units) = self.frequencyUnits(self.P.qeZero)
4413  autoUnitsIndex = self.qeZero_comboBox.findText(units)
4414 
4415  if autoFreqUnits: # Set automatic frequency units
4416  self.qeZero_lineEdit.setText(str(freq))
4417  self.qeZero_comboBox.setCurrentIndex(autoUnitsIndex)
4418  self.qeZeroUnitsIndex = autoUnitsIndex
4419  else: # Keep user frequency units
4420  userUnitsIndex = self.qeZero_comboBox.currentIndex()
4421  newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4422  self.qeZero_lineEdit.setText(str(newValue))
4423  self.qeZeroUnitsIndex = userUnitsIndex
4424  else:
4425  self.qeZeroUnitsIndex = self.qeZero_comboBox.currentIndex()
4426 
4427  if self.P.genChebyLr is not None: self.genChebyLr_lineEdit.setText(str(P.genChebyLr))
4428 
4429  #if self.P.transImagZeros is not None: self.transmissionZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.transImagZeros)))
4430 
4431  # P.validation() ensures that when there are no TZs or LPZs, lists are not None, but empty.
4432 
4433  listImaginaryPartsZeros = self.P.transImagZeros + np.imag( self.P.transComplexZeros + self.P.LPcomplexZeros ).tolist()
4434  if len(listImaginaryPartsZeros) > 0:
4435  (freq, units) = self.frequencyUnits(listImaginaryPartsZeros)
4436  autoUnitsIndex = self.transmissionZeros_comboBox.findText(units)
4437 
4438  if autoFreqUnits: # Set automatic frequency units
4439 # self.transmissionZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(freq)))
4440  self.transmissionZeros_comboBox.setCurrentIndex(autoUnitsIndex)
4441  self.transmissionZerosUnitsIndex = autoUnitsIndex
4442  else: # Keep user frequency units
4443  userUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
4444  self.transmissionZerosUnitsIndex = userUnitsIndex
4445  else:
4446  self.transmissionZerosUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
4447 
4448  if self.P.genChebyLP is not None: self.genChebyLP_groupBox.setChecked(bool(self.P.genChebyLP))
4449 # if self.P.LPcomplexZeros is not None: self.LPcomplexZeros_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.LPcomplexZeros)))
4450 
4451  if self.P.LPalgor is not None:
4452  algor = self.P.LPalgor.upper() # Uppercase letters for safe comparison with GUI ComboBox items
4453  iAlgor = self.LPalgor_comboBox.findText(algor)
4454  else:
4455  iAlgor = 4
4456 
4457  if iAlgor < 0:
4458  QMessageBox.warning(self, "WARNING", "Unknown Linear Phase Optimization Algorithm '%s' replaced by default value." % self.P.LPalgor)
4459  iAlgor = 4
4460  self.LPalgor_comboBox.setCurrentIndex(iAlgor)
4461 
4462  if self.P.LPmaxIter is not None: self.LPmaxIter_spinBox.setValue(self.P.LPmaxIter)
4463  if self.P.LPNsamples is not None: self.LPNsamples_spinBox.setValue(self.P.LPNsamples)
4464  if self.P.LPfracBW is not None: self.LPfracBW_doubleSpinBox.setValue(self.P.LPfracBW)
4465  if self.P.LPripple is not None: self.LPripple_doubleSpinBox.setValue(self.P.LPripple * 1e9)
4466 
4467  # Update table Widgets with TZs. self.updateTZsTable() resizes, fills and adjusts the table.
4468  TZsList = np.concatenate( (1j*np.array(self.P.transImagZeros), np.array(self.P.transComplexZeros)) ).tolist()
4469  self.updateTZsTable(self.genChebyTZs_tableWidget, TZsList)
4470  self.updateTZsTable(self.LPcomplexZeros_tableWidget, self.P.LPcomplexZeros + self.P.LPrealZeros)
4471 
4472  # Update total number of zeros in transmissionZeros_lcdNumber
4474 
4475  # Set Synthesis parameters widgets
4476  if self.P.synthTech is not None:
4477  tech = self.P.synthTech.title() # Capital initials for safe comparison with GUI ComboBox items
4478  iTech = self.synthTech_comboBox.findText(tech)
4479  else:
4480  iTech = 0
4481 
4482  if iTech < 0:
4483  QMessageBox.warning(self, "WARNING", "Unknown Synthesis Technique '%s' replaced by default value." % self.P.synthTech)
4484  iTech = 0
4485  self.synthTech_comboBox.setCurrentIndex(iTech)
4486  self.synthTech_stackedWidget.setCurrentIndex(iTech)
4487 
4488  # Lossless parameters
4489  if self.P.finiteQo is not None: self.finiteQo_groupBox.setChecked(bool(self.P.finiteQo))
4490  if self.P.Qo is not None: self.Qo_lineEdit.setText(self.float2stInf(self.P.Qo))
4491 
4492  # Predistortion parameters
4493  if self.P.zerosArrang is not None:
4494  if type(self.P.zerosArrang).__name__ == 'list': self.zerosArrang_comboBox.setCurrentIndex(0)
4495  else: self.zerosArrang_comboBox.setCurrentIndex(self.P.zerosArrang)
4496  if self.P.adapPredis is not None: self.adapPredis_groupBox.setChecked(bool(self.P.adapPredis))
4497  if self.P.QoPredis is not None: self.QoPredis_lineEdit.setText(self.float2stInf(self.P.QoPredis))
4498  if self.P.Qef is not None: self.Qef_lineEdit.setText(self.float2stInf(self.P.Qef))
4499 
4500  # Update lineEdit with weights list
4501  #if self.P.wFunc is not None: self.wFunc_lineEdit.setText(filter(lambda c: c not in "()[]", str(P.wFunc)))
4502 
4503  # Update table Widget with weights list. self.updatewFuncTableSize() resizes, fills and adjusts the table.
4504  self.updatewFuncTableSize(self.P.wFunc, (self.P.N, 1), True)
4505 
4506  # NU-Q Parameters
4507  if self.P.nuqK11c1 is not None: self.nuqK11c1_lineEdit.setText(str(self.P.nuqK11c1))
4508  if self.P.nuqK22c1 is not None: self.nuqK22c1_lineEdit.setText(str(self.P.nuqK22c1))
4509  if self.P.nuqK21c1 is not None: self.nuqK21c1_lineEdit.setText(str(self.P.nuqK21c1))
4510  if self.P.nuqK21c2 is not None: self.nuqK21c2_lineEdit.setText(str(self.P.nuqK21c2))
4511  if self.P.nuqK21c3 is not None: self.nuqK21c3_lineEdit.setText(str(self.P.nuqK21c3))
4512 
4513  if self.P.nuqTech is not None:
4514  nuqTech= self.P.nuqTech # No capital initials in this case
4515  iNuqTech = self.nuqTech_comboBox.findText(nuqTech)
4516  else:
4517  iNuqTech = 0
4518 
4519  if iNuqTech < 0:
4520  QMessageBox.warning(self, "WARNING", "Unknown Prescribed Insertion Loss technique '%s' replaced by default value." % self.P.nuqTech)
4521  iNuqTech = 0
4522  self.nuqTech_comboBox.setCurrentIndex(iNuqTech)
4523  self.nuqTech_stackedWidget.setCurrentIndex(iNuqTech)
4524 
4525  if self.P.nuqDelta is not None: self.nuqDelta_lineEdit.setText(str(self.P.nuqDelta))
4526 
4527  # Set sweep parameters widgets
4528  if self.P.minFreq is not None:
4529  (freq, units) = self.frequencyUnits(self.P.minFreq)
4530  autoUnitsIndex = self.minFreq_comboBox.findText(units)
4531 
4532  if autoFreqUnits: # Set automatic frequency units
4533  self.minFreq_lineEdit.setText(str(freq))
4534  self.minFreq_comboBox.setCurrentIndex(autoUnitsIndex)
4535  self.minFreqUnitsIndex = autoUnitsIndex
4536  else: # Keep user frequency units
4537  userUnitsIndex = self.minFreq_comboBox.currentIndex()
4538  newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4539  self.minFreq_lineEdit.setText(str(newValue))
4540  self.minFreqUnitsIndex = userUnitsIndex
4541  else:
4542  self.minFreqUnitsIndex = self.minFreq_comboBox.currentIndex()
4543 
4544  if self.P.maxFreq is not None:
4545  (freq, units) = self.frequencyUnits(self.P.maxFreq)
4546  autoUnitsIndex = self.maxFreq_comboBox.findText(units)
4547 
4548  if autoFreqUnits: # Set automatic frequency units
4549  self.maxFreq_lineEdit.setText(str(freq))
4550  self.maxFreq_comboBox.setCurrentIndex(autoUnitsIndex)
4551  self.maxFreqUnitsIndex = autoUnitsIndex
4552  else: # Keep user frequency units
4553  userUnitsIndex = self.maxFreq_comboBox.currentIndex()
4554  newValue = freq * pow(10, 3*(autoUnitsIndex-userUnitsIndex))
4555  self.maxFreq_lineEdit.setText(str(newValue))
4556  self.maxFreqUnitsIndex = userUnitsIndex
4557  else:
4558  self.maxFreqUnitsIndex = self.maxFreq_comboBox.currentIndex()
4559 
4560  if self.P.numFreq is not None: self.numFreq_spinBox.setValue(self.P.numFreq)
4561 
4562  self.oldSweep = (P.minFreq, P.maxFreq)
4563 
4564 
4565  ##
4566  #
4567  # Update predistorsion weights table Widget with list.
4568  # @param list = list containing entries to set in tableWidget
4569  #
4570  def updatewFuncTableEntries(self, list):
4571  for n in range(len(list)):
4572  #item = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n]) )
4573  item = QTableWidgetItem( complexStr(list[n], saveSignificantDigits, eng = True) )
4574 
4575  item.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4576  self.wFunc_tableWidget.setItem(0, n, item)
4577 
4578 
4579  ##
4580  #
4581  # Read list from predistorsion weights table Widget entries.
4582  # If row or column count is equal to 1, the 2D list is collapsed to a 1D list.
4583  # @return list = list containing entries in tableWidget
4584  #
4586  table = self.wFunc_tableWidget
4587  M = table.rowCount()
4588  list = []
4589  for m in range(M):
4590  item = table.item(m, 0)
4591  try:
4592  if item is None: raise ValueError, 'Empty cell'
4593  value = float(item.text())
4594  except ValueError, err:
4595  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4596  if item is not None: item.setText('')
4597  else: list.append(value)
4598  if len(list) == 1: list = list[0]
4599 
4600  return list
4601 
4602 
4603  ##
4604  #
4605  # Read list from TZs table Widget entries.
4606  # @param table = tableWidget to read
4607  # @return listImag = list containing purely imaginary zeros entries in tableWidget
4608  # @return listComplex = list containing complex zeros entries in tableWidget
4609  # @return listReal = list containing purely real zeros entries in tableWidget
4610  #
4611  def readTZsTableEntries(self, table):
4612 
4613  M = table.rowCount()
4614  listImag = []
4615  listComplex = []
4616  listReal = []
4617 
4618  for m in range(M):
4619  reItem = table.item(m, 0)
4620  stRe = str(reItem.text()).strip() if reItem is not None else ''
4621  try:
4622  re = float(stRe) if stRe != '' else 0
4623  except ValueError, err:
4624  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4625  table.currentItem().setText('')
4626  stRe = ''
4627  re = 0
4628 
4629  if re == 0 and stRe != '': table.currentItem().setText('') # Replace zeros by empty strings in table cells
4630 
4631  imItem = table.item(m, 1)
4632  stIm = str(imItem.text()).strip() if imItem is not None else ''
4633 
4634  if stIm != '': # Complex zero or purely imaginary
4635  try:
4636  im = float(stIm)
4637  except ValueError, err:
4638  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect table entry:<br>%s</p>' % err)
4639  table.currentItem().setText('')
4640  continue
4641 
4642  # DO NOT replace imaginary part by '': the former is a complex zero with 0 Hz imag part while the later is a real zero.
4643  # if im ==0: table.currentItem().setText('') # Replace zeros by empty strings in table cells
4644 
4645  if re==0 and im==0: continue # Empty table row
4646 
4647  stError = '<p align="center">Incorrect table entry at row %d:<br>%s</p><p align="center"><b>Please fix it.</b></p>'
4648  if im < 0:
4649  QMessageBox.critical(self, "ERROR", stError % (m+1, 'Imaginary part of a transmission zero cannot be negative Hz.'))
4650  if im == 0:
4651  QMessageBox.critical(self, "ERROR", stError % (m+1, 'Imaginary part of a transmission zero cannot be 0 Hz.'))
4652 
4653  # append zero to list
4654  if re == 0: listImag.append(im * pow(10, 3*self.transmissionZerosUnitsIndex))
4655  else: listComplex.append(re + 1j*im * pow(10, 3*self.transmissionZerosUnitsIndex))
4656 
4657  else: # Real zero
4658  if re==0: continue # Empty table row
4659  listReal.append(re)
4660 
4661  return listImag, listComplex, listReal
4662 
4663 
4664  ##
4665  #
4666  # Updates a TZs tableWidget with contents of list, and
4667  # makes the number of rows equal to the number of zeros.
4668  # Adjusts widget size to table contents.
4669  # This function is called by the self.updateDialogParams() function.
4670  # @param table = tableWidget to update
4671  # @param list = list containing entries to set in tableWidget
4672  #
4673  def updateTZsTable(self, table, list):
4674  table.setRowCount(len(list))
4675  table.setColumnCount(3)
4676  stUnits = self.transmissionZeros_comboBox.itemText(self.transmissionZerosUnitsIndex)
4677  table.setHorizontalHeaderLabels( ['Real', 'Imag (%s)' % stUnits, 'Imag-f0 (%s)' % stUnits ] )
4678  table.setToolTip(unicode(table.toolTip()).replace(table.units, stUnits))
4679  table.setUnits(stUnits)
4680 
4681  # Table is not being updated by the user!
4682  table.updatedByUser = False
4683 
4684  for n in range(len(list)):
4685  if list[n].real != 0: reItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n].real) )
4686  else: reItem = QTableWidgetItem('')
4687  reItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4688  table.setItem(n, 0, reItem)
4689 
4690  if list[n].imag != 0:
4691  imItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, list[n].imag / pow(10, 3*self.transmissionZerosUnitsIndex)) )
4692  imItemf0 = QTableWidgetItem( '%.*g' % (saveSignificantDigits, (list[n].imag - table.f0) / pow(10, 3*self.transmissionZerosUnitsIndex)) )
4693  else:
4694  imItem = QTableWidgetItem('')
4695  imItemf0 = QTableWidgetItem('')
4696  imItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4697  imItemf0.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
4698  table.setItem(n, 1, imItem)
4699  table.setItem(n, 2, imItemf0)
4700 
4701  table.updatedByUser = True
4702 
4703  table.updateTableSize()
4704 
4705 
4706  ##
4707  #
4708  # Returns frequency expressed in GHz, MHz, 'kHz' or Hz.
4709  # @param freq = Frequency in Hz. Can be a float or a list of floats.
4710  # @return (newFreq, units).
4711  # @n newFreq = Frequency expressed in units.
4712  # @n units = 'GHz', 'MHz', 'kHz' or 'Hz'.
4713  #
4714  def frequencyUnits(self, freq):
4715  if freq == 0: return (0, 'Hz')
4716 
4717  if type(freq) in [ types.FloatType, types.IntType, types.ComplexType ]: freq = [ freq ]
4718 
4719  afreq = np.array(freq)
4720  freqExp = log10(np.prod(afreq)) / len(freq)
4721 
4722  if freqExp >= 9:
4723  newFreq = afreq / 1e9
4724  units = 'GHz'
4725  elif freqExp >= 6:
4726  newFreq = afreq / 1e6
4727  units = 'MHz'
4728  elif freqExp >= 3:
4729  newFreq = afreq / 1e3
4730  units = 'kHz'
4731  else:
4732  newFreq = afreq
4733  units = 'Hz'
4734 
4735  newFreq = newFreq.tolist()
4736  if len(newFreq) == 1: newFreq = newFreq[0]
4737 
4738  return newFreq, units
4739 
4740 
4741  ##
4742  #
4743  # Convert string to float. 'INF', 'Inf', 'inf' are converted to numpy.inf.
4744  # @param st = String to convert.
4745  # @return value = Numerical value of string (float).
4746  #
4747  def st2floatInf(self, st):
4748  return np.inf if st.lower().lstrip().rstrip()=='inf' else float(st)
4749 
4750 
4751  ##
4752  #
4753  # Convert float to string. numpy.inf is converted to 'Inf'.
4754  # @param value = Float value to convert.
4755  # @return st = String.
4756  #
4757  def float2stInf(self, value):
4758  return 'Inf' if value==np.inf else str(value)
4759 
4760 
4761  ##
4762  #
4763  # Reads parameter values from dialog and store in self.P (dialog working copy of parameters).
4764  # Error control only for invalid parameter values that would crash the parser or the GUI.
4765  # Empty lineEdit boxes are allowed in parameters that will not be used in the computation.
4766  # More error control in lossyfilters.Parameters.validate() .
4767  # @return ok (True / False) = If True, parameters have been read correctly, if False, there has been an error.
4768  #
4769  def readDialogParams(self):
4770 
4771  try:
4772  ##
4773  # key is used to store parameter name for error catching
4774  key = 'Filter / Filter order'
4775  self.P.N = self.N_spinBox.value()
4776 
4777  key = 'Name of results output files'
4778  if self.outName_lineEdit.text() == u'': raise ValueError, 'Empty string'
4779  self.P.outName = unicode(self.outName_lineEdit.text())
4780 
4781  key = 'Output directory'
4782  if self.outDir_lineEdit.text() == u'': self.P.outDir = None
4783  self.P.outDir = unicode(self.outDir_lineEdit.text())
4784 
4785  key = 'Filter / Center Frequency'
4786  self.P.f0 = float(self.f0_lineEdit.text()) * 10**(3*self.f0UnitsIndex)
4787 
4788  key = 'Filter / Bandwidth'
4789  self.P.BW = float(self.BW_lineEdit.text()) * 10**(3*self.BWUnitsIndex)
4790 
4791  key = 'Filter / Filter Topology'
4792  self.P.filterTransferFunc = unicode(self.topol_comboBox.currentText())
4793 
4794  key = 'Filter / Chebyshev / Return losses'
4795  if unicode(self.chebyLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Chebyshev':
4796  self.P.chebyLr = float(self.chebyLr_lineEdit.text())
4797 
4798  key = 'Filter / Quasieliptic / Return losses'
4799  if unicode(self.qeLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Quasieliptic':
4800  self.P.qeLr = float(self.qeLr_lineEdit.text())
4801 
4802  key = 'Filter / Quasieliptic / Transmission zero'
4803  if unicode(self.qeZero_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Quasieliptic':
4804  self.P.qeZero = float(self.qeZero_lineEdit.text()) * 10**(3*self.qeZeroUnitsIndex)
4805 
4806  key = 'Filter / Generalized Chebyshev / Return losses'
4807  if unicode(self.genChebyLr_lineEdit.text()) != u'' or self.P.filterTransferFunc == u'Generalized Chebyshev':
4808  self.P.genChebyLr = float(self.genChebyLr_lineEdit.text())
4809 
4810  key = 'Filter / Generalized Chebyshev / Transmission Zeros'
4811 
4812  # Allow an empty transmission zero list. Generalized Chebyshev will be equivalent to Chebyshev design.
4813  if self.P.filterTransferFunc == u'Generalized Chebyshev':
4814 
4815  #self.P.transImagZeros = [ n * 10**(3*self.transmissionZerosUnitsIndex) for n in map(float, str(self.transmissionZeros_lineEdit.text()).split(',')) ] if unicode(self.transmissionZeros_lineEdit.text()).lstrip() != u'' else [ ]
4816 
4817  # Read transmission zeros and separate in list of complex and list of imaginary transmission zeros
4818  self.P.transImagZeros, self.P.transComplexZeros, transRealZeros = self.readTZsTableEntries(self.genChebyTZs_tableWidget)
4819  if len(transRealZeros) > 0: raise ValueError, 'Transmission zeros cannot be purely real,\nthey must have imaginary part > 0 Hz'
4820 
4821  # For list of complex
4822  #self.P.transImagZeros = map(complex, str(self.transmissionZeros_lineEdit.text()).replace('i', 'j').split(',')) if unicode(self.transmissionZeros_lineEdit.text()) != u'' else [ ]
4823 
4824  key = 'Filter / Generalized Chebyshev / Linear Phase'
4825  self.P.genChebyLP = int(self.genChebyLP_groupBox.isChecked())
4826 
4827  if self.P.genChebyLP:
4828 # key = 'Filter / Generalized Chebyshev / Linear Phase / Complex transmission zeros'
4829 # self.P.LPcomplexZeros = map(complex, str(self.LPcomplexZeros_lineEdit.text()).replace(' ', '').replace('i', 'j').split(',')) if unicode(self.LPcomplexZeros_lineEdit.text()) != u'' else [ ]
4830 
4831 # # Read a list of real numbers
4832 # key = 'Filter / Generalized Chebyshev / Linear Phase / Real transmission zeros'
4833 # self.P.LPrealZeros = map(float, str(self.LPrealZeros_lineEdit.text()).split(',')) if unicode(self.LPrealZeros_lineEdit.text()) != u'' else [ ]
4834 
4835  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Algorithm'
4836  self.P.LPalgor = unicode(self.LPalgor_comboBox.currentText())
4837 
4838  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Max iter'
4839  self.P.LPmaxIter = self.LPmaxIter_spinBox.value()
4840 
4841  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / N samples'
4842  self.P.LPNsamples = self.LPNsamples_spinBox.value()
4843 
4844  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Fractional bandwidth to optimize'
4845  self.P.LPfracBW = self.LPfracBW_doubleSpinBox.value()
4846 
4847  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Group delay ripple'
4848  self.P.LPripple = self.LPripple_doubleSpinBox.value() / 1e9
4849 
4850  key = 'Filter / Generalized Chebyshev / Linear Phase equalization / Initial position of complex transmission zeros'
4851  imagZerosList, self.P.LPcomplexZeros, self.P.LPrealZeros = self.readTZsTableEntries(self.LPcomplexZeros_tableWidget)
4852  if len(imagZerosList) > 0: raise ValueError, 'Linear Phase optimization zeros cannot be purely imaginary'
4853 
4854  key = 'Synthesis / Synthesis Technique'
4855  self.P.synthTech = unicode(self.synthTech_comboBox.currentText())
4856 
4857  key = 'Synthesis / Minimum Insertion Loss / Finite Q'
4858  self.P.finiteQo = int(self.finiteQo_groupBox.isChecked())
4859 
4860  key = 'Synthesis / Minimum Insertion Loss / Q of implementation'
4861  if unicode(self.Qo_lineEdit.text()) != u'' or ( self.P.synthTech == u'Minimum Insertion Loss' and self.P.finiteQo == 1 ):
4862  self.P.Qo = self.st2floatInf(unicode(self.Qo_lineEdit.text()))
4863 
4864  key = 'Synthesis / Predistortion / Arrangement of the reflection zeros'
4865  if self.zerosArrang_comboBox.currentIndex() == 0:
4866  # If self.P.zerosArrang has been changed from 'int' to user-selected, create new list of bool, otherwise do not touch it
4867  if type(self.P.zerosArrang).__name__ == 'int':
4868  if self.P.zerosArrang == 1: self.P.zerosArrang = [ True for n in range(self.P.N) ]
4869  if self.P.zerosArrang == 2: self.P.zerosArrang = [ False for n in range(self.P.N) ]
4870  if self.P.zerosArrang == 3: self.P.zerosArrang = [ bool(n%2) for n in range(self.P.N) ]
4871  if self.P.zerosArrang == 4: self.P.zerosArrang = [ not bool(n%2) for n in range(self.P.N) ]
4872  else:
4873  self.P.zerosArrang = self.zerosArrang_comboBox.currentIndex()
4874 
4875  key = 'Synthesis / Predistortion / Adaptive predistorsion'
4876  self.P.adapPredis = int(self.adapPredis_groupBox.isChecked())
4877 
4878  key = 'Synthesis / Predistortion / Q to implement'
4879  if unicode(self.QoPredis_lineEdit.text()) != u'' or self.P.synthTech == u'Predistortion':
4880  self.P.QoPredis = self.st2floatInf(unicode(self.QoPredis_lineEdit.text()))
4881 
4882  key = 'Synthesis / Predistortion / Q to emulate'
4883  if unicode(self.Qef_lineEdit.text()) != u'' or self.P.synthTech == u'Predistortion':
4884  self.P.Qef = self.st2floatInf(unicode(self.Qef_lineEdit.text()))
4885 
4886  key = 'Synthesis / Predistortion / Weight function for the pole shifting'
4887 
4888  if self.P.synthTech == u'Predistortion' and self.P.adapPredis == 1:
4889 # # Read weights from lineEdit into list
4890 # if unicode(self.wFunc_lineEdit.text()) != u'': self.P.wFunc = map(float, str(self.wFunc_lineEdit.text()).split(','))
4891 
4892  # Read weights from tableWidget into list
4893  self.P.wFunc = self.readwFuncTableEntries()
4894 
4895  # Convert one-element list to float. It is the amplitude of Ming-Yu sinusoidal function.
4896  if len(self.P.wFunc) == 1: self.P.wFunc = self.P.wFunc[0]
4897 
4898  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique'
4899  self.P.nuqTech = unicode(self.nuqTech_comboBox.currentText())
4900 
4901  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K11'
4902  if unicode(self.nuqK11c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4903  self.P.nuqK11c1 = float(self.nuqK11c1_lineEdit.text())
4904 
4905  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K22'
4906  if unicode(self.nuqK22c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4907  self.P.nuqK22c1 = float(self.nuqK22c1_lineEdit.text())
4908 
4909  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+kS11 / K21'
4910  if unicode(self.nuqK21c1_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+kS11':
4911  self.P.nuqK21c1 = float(self.nuqK21c1_lineEdit.text())
4912 
4913  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21 / Insertion losses K21'
4914  if unicode(self.nuqK21c2_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21':
4915  self.P.nuqK21c2 = float(self.nuqK21c2_lineEdit.text())
4916 
4917  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+pole/zero / Insertion losses K21'
4918  if unicode(self.nuqK21c3_lineEdit.text()) != u'' or self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+pole/zero':
4919  self.P.nuqK21c3 = float(self.nuqK21c3_lineEdit.text())
4920 
4921  key = 'Synthesis / Non-uniform Q / Prescribed Insertion Loss technique / kS21+pole/zero / Delta'
4922  if unicode(self.nuqDelta_lineEdit.text()) != u'' or (self.P.synthTech == u'Prescribed Insertion Loss' and self.P.nuqTech == 'kS21+pole/zero' ):
4923  self.P.nuqDelta = float(self.nuqDelta_lineEdit.text())
4924 
4925  key = 'Sweep / Minimum Frequency'
4926  self.P.minFreq = float(self.minFreq_lineEdit.text()) * 10**(3*self.minFreqUnitsIndex)
4927 
4928  key = 'Sweep / Maximum Frequency'
4929  self.P.maxFreq = float(self.maxFreq_lineEdit.text()) * 10**(3*self.maxFreqUnitsIndex)
4930 
4931  key = 'Sweep / Number of samples'
4932  self.P.numFreq = self.numFreq_spinBox.value()
4933 
4934  if self.oldSweep != (self.P.minFreq, self.P.maxFreq): self.mainWindow.autoScaleFreq = True
4935  else: self.mainWindow.autoScaleFreq = False
4936 
4937  self.oldSweep = (self.P.minFreq, self.P.maxFreq)
4938 
4939  except ValueError, err:
4940  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect parameter error:<br>%s<br>%s<br></p><p align="center">Fix or click "Discard" button.</p>' % (key, err))
4941  return False
4942 
4943  try:
4944  self.P.validate()
4945 
4946  except parseError, err:
4947  QMessageBox.critical(self, "ERROR", '<p align="center">Incorrect parameter error:<br>%s</p><p align="center">Fix or click "Discard" button.</p>' % err)
4948  return False
4949 
4950  # Recompute FT with updated filter parameters
4951  self.mainWindow.FT = FrequencyTransformBP(self.P)
4952 
4953  # Only at the end P can be considered non-empty
4954  self.P.empty = False
4955 
4956 
4957  return True
4958 
4959 
4960  ##@{
4961  # @name Functions automatically excuted when the user interacts with the GUI
4962 
4963  ##
4964  #
4965  # Update the size of the predistortion zeros arrangement list after the user changes the filter order.
4966  # @param N = Filter order as in self.N_spinBox.value()
4967  #
4969  #N = self.N_spinBox.value()
4970  if type(self.P.zerosArrang).__name__ == 'list':
4971  Nold = len(self.P.zerosArrang)
4972  if N > Nold: self.P.zerosArrang += [ False for n in range(N-Nold)]
4973  if N < Nold: self.P.zerosArrang = self.P.zerosArrang[0:N]
4974 
4975 
4976  ##
4977  #
4978  # Make the size of the adaptive predistorsion weights table equal to the order of the filter.
4979  # Calls self.updatewFuncTableEntries() function and adjusts widget size to table contents.
4980  # This function is automatically executed by the GUI every time the user changes the filter order in the GUI or activates the adaptive predistiortion checkbox,
4981  # but only if the synthesis method is 'Predistortion' and adaptive predistortion group box is checked.
4982  # and is called by the self.updateDialogParams() function.
4983  # @param list = list containing entries to set in tableWidget
4984  # @param dims = tuple (rows, cols) contaning number of rows and columns
4985  # @param updateEntries = If True, table entries are updated (calls self.updatewFuncTableEntries(list)).
4986  # When this function is called by itemChanged signal, this flag must be False to avoid infinite recursion.
4987  #
4988  def updatewFuncTableSize(self, list, dims, updateEntries):
4989  if unicode(self.synthTech_comboBox.currentText()) == u'Predistortion' and self.adapPredis_groupBox.isChecked():
4990 
4991  if updateEntries:
4992  M = dims[0] # Filter order
4993  N = dims[1] # Equal to one column
4994  assert N==1
4995  if len(list) == 0:
4996  list = (1 - 0.5 * np.sin(np.arange(1., M+1)*pi/(M+1)) )
4997  self.wFunc_tableWidget.setRowCount(M)
4998  self.wFunc_tableWidget.setColumnCount(N)
4999  self.updatewFuncTableEntries(list)
5000 
5001  self.wFunc_tableWidget.resizeRowsToContents()
5002  self.wFunc_tableWidget.resizeColumnsToContents()
5003  self.wFunc_tableWidget.setMaximumSize( self.wFunc_tableWidget.maximumSizeHint() )
5004 
5005 
5006  ##
5007  #
5008  # Count the number of Generalized Chebyshev transmission zeros set in the GUI widgets and update the lcdNumber widget.
5009  # The dialog window is automatically resized to fit the rows corresponding to TZs.
5010  # This function is automatically executed by the GUI every time the user changes one of the zeros lists in the GUI.
5011  #
5013 
5014  self.P.transImagZeros, self.P.transComplexZeros, transRealZeros = self.readTZsTableEntries(self.genChebyTZs_tableWidget)
5015  LPimagZeros, self.P.LPcomplexZeros, self.P.LPrealZeros = self.readTZsTableEntries(self.LPcomplexZeros_tableWidget)
5016 
5017  totalZeros = len(self.P.transImagZeros) + 2*len(self.P.transComplexZeros)
5018 
5019  if self.genChebyLP_groupBox.isChecked():
5020  totalZeros += len(LPimagZeros) + 2*len(self.P.LPcomplexZeros + self.P.LPrealZeros)
5021 
5022  self.transmissionZeros_lcdNumber.display(totalZeros)
5023 
5024  # Update dialog size to fit table
5025  self.updateDlgHeight()
5026 
5027 
5028  ##
5029  #
5030  # Update dialog size to fit table
5031  #
5032  def updateDlgHeight(self):
5033 
5034  incTZsTable = (self.genChebyTZs_tableWidget.rowCount()+1.5) * self.genChebyTZs_tableWidget.rowHeight(0)
5035  incLPzerosTable = (self.LPcomplexZeros_tableWidget.rowCount()+1.5) * self.LPcomplexZeros_tableWidget.rowHeight(0)
5036 
5037  dlgSize = self.size()
5038  dlgHeight= dlgSize.height()
5039  dlgMinHeight = self.dlgInitSize
5040  dlgOptSize = dlgMinHeight + incTZsTable + incLPzerosTable
5041 # print 'incTZs =', incTZsTable, '/ incLP =', incLPzerosTable, '/ Height =', dlgHeight, '/ MinHeight =', dlgMinHeight, '/ OptSize =', dlgOptSize
5042 # print 'TZ Row =', self.genChebyTZs_tableWidget.rowHeight(0), '/ LP Row =', self.LPcomplexZeros_tableWidget.rowHeight(1)
5043 # print 'TZscrollH = ', self.genChebyTZs_tableWidget.horizontalScrollBar().height(), '/ LPscrollH = ', self.LPcomplexZeros_tableWidget.horizontalScrollBar().height()
5044 # print ''
5045  #if dlgHeight < dlgOptSize:
5046  dlgSize.setHeight(dlgOptSize)
5047  self.resize(dlgSize)
5048 
5049 
5050  ##
5051  #
5052  # Custom slot in Qt designer conected to widgets "changed" signal.
5053  # This function is automatically executed by the GUI every time the user changes dialog values,
5054  # and sets the self.dialogModified variable to True and self.recomputed to False.
5055  #
5056  def dialogChanged(self):
5057  self.dialogModified = True
5058  self.recomputed = False
5059  if hasattr(self.sender(), 'validator'):
5060  validator = self.sender().validator()
5061  if validator is not None:
5062  result = validator.validate(self.sender().text(), 0)[0]
5063  palette = self.sender().palette()
5064  if result == 1:
5065  palette.setColor(QPalette.Text, Qt.red)
5066  self.sender().setPalette(palette)
5067  self.badInput.append(self.sender())
5068  self.sender().setFocus(Qt.OtherFocusReason) # It does nothing, because it is executed before focus changes to another widget.
5069  if result == 2:
5070  palette.setColor(QPalette.Text, self.mainWindow.palette().text().color())
5071  self.sender().setPalette(palette)
5072  while self.sender() in self.badInput: self.badInput.remove(self.sender())
5073 
5074  # For debugging
5075  #print [ unicode(n.text()) for n in self.badInput]
5076 
5077  if len(self.badInput) > 0:
5078  self.compute_pushButton.setEnabled(False)
5079  self.accept_pushButton.setEnabled(False)
5080  else:
5081  self.compute_pushButton.setEnabled(True)
5082  self.accept_pushButton.setEnabled(True)
5083 
5084 
5085  @pyqtSignature("")
5086  ##
5087  #
5088  # Opens getExistingDirectory Qt dialog in order to set P.outDir variable.
5089  #
5091  outDir = self.P.outDir if self.P.outDir is not None else os.path.dirname(sys.argv[0])
5092  self.P.outDir = QFileDialog.getExistingDirectory(self, QString(u"Output directory"), outDir)
5093  if self.P.outDir:
5094  self.outDir_lineEdit.setText(QDir.toNativeSeparators(self.P.outDir))
5095 
5096 
5097  @pyqtSignature("bool")
5098  ##
5099  #
5100  # Run imaginary TZ and bandwidth optimization to comply specification mask.
5101  #
5102  def on_TZoptim_pushButton_clicked(self, checked):
5103 
5104  # Read parameters in order to update self.P and update self.mainWindow.FT. The updated parameters can still be discarded with the 'Discard' pushbutton.
5105  if self.readDialogParams() is False:
5106  QMessageBox.critical(self, "ERROR", 'Incorrect parameter in edit window.<p>Cannot run optimization with incorrect parameters.')
5107  return
5108 
5109  algor = self.algor_comboBox.currentText()
5110  maxIter = self.maxIter_spinBox.value()
5111  Nsamples = self.Nsamples_spinBox.value()
5112  BWmaxvar = self.BWoptim_doubleSpinBox.value()
5113  weights = [ self.wS11_doubleSpinBox.value(), self.wS22_doubleSpinBox.value(), self.wS21_doubleSpinBox.value() ]
5114 
5115  SM = self.mainWindow.SM
5116 
5117  if SM is None:
5118  reply = QMessageBox.question(self, "WARNING", '<b>No specification mask loaded.</b><p>Do you want to open an specification mask file now?', QMessageBox.Yes| QMessageBox.Cancel)
5119  if reply == QMessageBox.Yes:
5120  self.mainWindow.on_action_Mask_triggered(True)
5121  SM = self.mainWindow.SM
5122 
5123  runOptimization = False
5124  if SM is not None:
5125  if self.genChebyLP_groupBox.isChecked():
5126  reply = QMessageBox.question(self, "WARNING", '''<b>Linear phase optimization is enabled.</b><p>
5127  First, equalization zeros will be optimized for linear phase.<p>
5128  Second, imaginary transmission zeros will be optimized to comply mask, with the optimum equalization zeros as prescribed transmission zeros.<p>
5129  Third, if you have an open [S] parameter window, filter synthesis will be recomputed, which involves a new linear phase optimization with fixed prescribed transmission zeros.<p>
5130  Do you wish to proceed?''', QMessageBox.Yes| QMessageBox.Cancel)
5131  if reply == QMessageBox.Yes:
5132  runOptimization = True
5133  LPeqZeros = True
5134  else:
5135  runOptimization = True
5136  LPeqZeros = False
5137 
5138  if runOptimization:
5139 
5140  # First check if library libextrafilters.py has been imported
5141  if importedExtraFilters is False:
5142  if existingExtraFilters is False:
5143  criticalErrorMsg("""<b>ERROR in Filter Synthesis:</b><p>
5144  Chebyshev filter is in the Library of Extra Filters,<br>
5145  which is not installed in this system.
5146  <p>Contact lossyfilters@tsc.upc.edu to get the Library of Extra Filters.
5147  """)
5148  else:
5149  criticalErrorMsg("""<b>ERROR in Filter Synthesis:</b><p>
5150  Importing the 'Library of Extra Filters' failed. <br>
5151  You have a libextrafilters.pyc file, but it cannot be imported. <br>
5152  Possibly libextrafilters.pyc was compiled using a different Python version than yours.
5153  <p>Your installed Python version is: %s on %s
5154  <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5155  """ % (platform.python_version(), platform.system()) )
5156  # Return without running optimization
5157  return
5158 
5159  app.setOverrideCursor(Qt.WaitCursor)
5160  try:
5161  TZoptimMask(self, self.P, self.mainWindow.FT, weights, BWmaxvar, algor, maxIter, Nsamples, SM, LPeqZeros)
5162  except synthError, err:
5163  criticalErrorMsg('<b>ERROR in Filter Synthesis:</b><p>' + unicode(err))
5164 
5165  app.restoreOverrideCursor()
5166  myPrint('Imaginaty TZs and BW optimization to mask with parameters: alg=%s, maxIter=%d, Nsamples=%d, BWmaxvar=%g, w=%s, mask=%s' % (algor, maxIter, Nsamples, BWmaxvar, listStr(weights, 2), SM.fileName) )
5167  else:
5168  myPrint('Imaginaty TZs and BW optimization to mask cancelled by user')
5169 
5170 
5171  @pyqtSignature("bool")
5172  ##
5173  #
5174  # Run synthesis. If parameters are valid, self.mainWindow.on_action_Execute_triggered() will set self.recomputed to True.
5175  #
5176  def on_compute_pushButton_clicked(self, checked):
5177  # Launch synthesis from Main Window
5178  self.mainWindow.on_action_Execute_triggered(True)
5179 
5180 
5181  @pyqtSignature("bool")
5182  ##
5183  #
5184  # Add new zero (blank) to transmission zeros table.
5185  #
5187  self.genChebyTZs_tableWidget.addRow()
5188 
5189 
5190  @pyqtSignature("bool")
5191  ##
5192  #
5193  # Add new zero (blank) to LP complex zeros table.
5194  #
5196  self.LPcomplexZeros_tableWidget.addRow()
5197 
5198 
5199  @pyqtSignature("bool")
5200  ##
5201  #
5202  # Add new zero with symmetrical imaginary part (in normalized frequency) to transmission zeros table.
5203  #
5204  def on_TZs_symm_pushButton_clicked(self, checked):
5205  table = self.genChebyTZs_tableWidget
5206  row = table.currentRow()
5207 
5208  if row == -1:
5209  stError = '<p align="center">Please select a transmission zero to symmetrize<p align="center"</p>'
5210  QMessageBox.warning(self, "WARNING", stError)
5211  return
5212 
5213  reItem = table.item(row, 0)
5214  imItem = table.item(row, 1)
5215  stRe = str(reItem.text()).strip() if reItem is not None else ''
5216  stIm = str(imItem.text()).strip() if imItem is not None else ''
5217  re = float(stRe) if stRe != '' else 0
5218  im = float(stIm) * pow(10, 3*self.transmissionZerosUnitsIndex) if stIm != '' else 0
5219 
5220  # This does not work in case f0_lineEdit has changed, if you do not update FT first
5221  #imSym = self.mainWindow.FT.unormFreq( - self.mainWindow.FT.normFreq(im) )
5222  f0 = float(self.f0_lineEdit.text()) * 10**(3*self.f0UnitsIndex)
5223  imSym = f0**2 / im
5224 
5225  N = table.rowCount()
5226  table.setRowCount(N+1)
5227  imSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, imSym / pow(10, 3*self.transmissionZerosUnitsIndex)) )
5228  imSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5229  table.setItem(N, 1, imSymItem)
5230  reSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, re) ) if re != 0 else QTableWidgetItem('')
5231  reSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5232  table.setItem(N, 0, reSymItem)
5233 
5234  table.updateTableSize()
5236 
5237 
5238  @pyqtSignature("bool")
5239  ##
5240  #
5241  # Add new zero with symmetrical imaginary part (in normalized frequency) to transmission zeros table.
5242  #
5243  def on_LP_symm_pushButton_clicked(self, checked):
5244  table = self.LPcomplexZeros_tableWidget
5245  row = table.currentRow()
5246 
5247  if row == -1:
5248  stError = '<p align="center">Please select a transmission zero to symmetrize<p align="center"</p>'
5249  QMessageBox.warning(self, "WARNING", stError)
5250  return
5251 
5252  reItem = table.item(row, 0)
5253  imItem = table.item(row, 1)
5254  stRe = str(reItem.text()).strip() if reItem is not None else ''
5255  stIm = str(imItem.text()).strip() if imItem is not None else ''
5256  re = float(stRe) if stRe != '' else 0
5257  im = float(stIm) * pow(10, 3*self.transmissionZerosUnitsIndex) if stIm != '' else 0
5258 
5259  imSym = self.mainWindow.FT.unormFreq( - self.mainWindow.FT.normFreq(im) )
5260 
5261  N = table.rowCount()
5262  table.setRowCount(N+1)
5263  imSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, imSym / pow(10, 3*self.transmissionZerosUnitsIndex)) )
5264  imSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5265  table.setItem(N, 1, imSymItem)
5266  reSymItem = QTableWidgetItem( '%.*g' % (saveSignificantDigits, re) ) if re != 0 else QTableWidgetItem('')
5267  reSymItem.setTextAlignment(Qt.AlignRight|Qt.AlignVCenter)
5268  table.setItem(N, 0, reSymItem)
5269 
5270  table.updateTableSize()
5272 
5273 
5274  ##
5275  #
5276  # Save current dialog parameters to mainWindow.P and close dialog.
5277  # This function is automatically executed by the GUI when the user clickes the 'Accept' button or closes the dialog.
5278  # Closing the dialog by killing the window is equivalent to clicking the 'Accept' button.
5279  #
5280  def closeEvent(self, event):
5281  if self.rejected:
5282  event.accept()
5283  else:
5284  # Copy dialog parameters to mainWindow.P only if there is no incorrect parameter
5285  if self.readDialogParams() is True:
5286 
5287  if self.dialogModified:
5288  if not self.recomputed:
5289  message = "Parameters have been modified and it is necessary to recompute synthesis.<p>Continue?"
5290  reply = QMessageBox.question(self, "%s - Recompute" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
5291  if reply in [QMessageBox.Cancel, QMessageBox.No]:
5292  event.ignore()
5293  return
5294  elif reply == QMessageBox.Yes:
5296  # If parameters have been modified and closeEvent is not ignored, mainWindow.dirty must be True
5297  self.mainWindow.dirty = True
5298  self.mainWindow.setWindowModified(self.mainWindow.dirty)
5299 
5300  self.mainWindow.updateStatus("Parameter values accepted")
5301  self.mainWindow.paramEdit = None
5302  event.accept()
5303  else:
5304  self.mainWindow.updateStatus("Incorrect parameters not accepted")
5305  event.ignore()
5306 
5307 
5308  @pyqtSignature("bool")
5309  ##
5310  #
5311  # Reject current dialog parameters and close dialog.
5312  # A backup copy of parameters and synthesis results is recovered.
5313  # This function is automatically executed by the GUI when the user clickes the 'Discard' button.
5314  #
5315  def on_discard_pushButton_clicked(self, checked):
5316  if self.dialogModified or self.recomputed:
5317  message = """Dialog discarded by user:<p>
5318  All parameters and synthesis results will be reverted to a previous state. To keep your parameters and results, click the "Accept" button in the Parameter Editor.<p>
5319  Revert parameters and results?"""
5320  reply = QMessageBox.question(self, "%s - Discard parameters" % applicationName, message, QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
5321  if reply in [QMessageBox.Cancel, QMessageBox.No]:
5322  return
5323 
5324  (self.mainWindow.P, self.mainWindow.CP, self.mainWindow.CM, self.mainWindow.SP, self.mainWindow.FT) = copy.deepcopy(self.resultBkp)
5325  self.mainWindow.updateStatus("Parameter values discarded by user")
5326  self.mainWindow.paramEdit = None
5327  self.mainWindow.updateWindows() # After setting self.mainWindow.paramEdit = None, to avoid updating paramEdit unnecessarily-
5328  self.rejected = True
5329  self.close()
5330 
5331  # reject() only works if window is a QDialog.
5332  #self.reject()
5333 
5334 
5335  @pyqtSignature("int")
5336  ##
5337  #
5338  # Update lineEdit value when the units ComboBox is changed.
5339  # This function is automatically executed by the GUI when the user changes the filter frequency units ComboBox.
5340  #
5342  #newIndex = self.f0_comboBox.currentIndex()
5343  newIndex = value
5344  if self.f0UnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5345  newValue = float(self.f0_lineEdit.text()) * pow(10, 3*(self.f0UnitsIndex-newIndex))
5346  self.f0_lineEdit.setText(str(newValue))
5347  self.f0UnitsIndex = newIndex
5348 
5349 
5350  @pyqtSignature("int")
5351  ##
5352  #
5353  # Update lineEdit value when the units ComboBox is changed.
5354  # This function is automatically executed by the GUI when the user changes the filter bandwidht units ComboBox.
5355  #
5357  #newIndex = self.BW_comboBox.currentIndex()
5358  newIndex = value
5359  if self.BWUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5360  newValue = float(self.BW_lineEdit.text()) * pow(10, 3*(self.BWUnitsIndex-newIndex))
5361  self.BW_lineEdit.setText(str(newValue))
5362  self.BWUnitsIndex = newIndex
5363 
5364 
5365  @pyqtSignature("int")
5366  ##
5367  #
5368  # Update lineEdit value when the units ComboBox is changed.
5369  # This function is automatically executed by the GUI when the user changes the Quasieliptic zeros frequency units ComboBox.
5370  #
5372  #newIndex = self.qeZero_comboBox.currentIndex()
5373  newIndex = value
5374  if self.qeZeroUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5375  newValue = float(self.qeZero_lineEdit.text()) * pow(10, 3*(self.qeZeroUnitsIndex-newIndex))
5376  self.qeZero_lineEdit.setText(str(newValue))
5377  self.qeZeroUnitsIndex = newIndex
5378 
5379 
5380  @pyqtSignature("int")
5381  ##
5382  #
5383  # Update lineEdit value when the units ComboBox is changed.
5384  # This function is automatically executed by the GUI when the user changes the Generalized Chebyshev zeros frequency units ComboBox.
5385  #
5387 # self.transmissionZerosUnitsIndex = self.transmissionZeros_comboBox.currentIndex()
5388  self.transmissionZerosUnitsIndex = value
5389 
5390  # Update table Widgets using new self.transmissionZerosUnitsIndex
5391  TZsList = np.concatenate( (1j*np.array(self.P.transImagZeros), np.array(self.P.transComplexZeros)) ).tolist()
5392  self.updateTZsTable(self.genChebyTZs_tableWidget, TZsList)
5393  self.updateTZsTable(self.LPcomplexZeros_tableWidget, self.P.LPcomplexZeros + self.P.LPrealZeros)
5394 
5395 
5396  @pyqtSignature("int")
5397  ##
5398  #
5399  # Update lineEdit value when the units ComboBox is changed.
5400  # This function is automatically executed by the GUI when the user changes the minimum sweep frequency units ComboBox.
5401  #
5403  #newIndex = self.minFreq_comboBox.currentIndex()
5404  newIndex = value
5405  if self.minFreqUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5406  newValue = float(self.minFreq_lineEdit.text()) * pow(10, 3*(self.minFreqUnitsIndex-newIndex))
5407  self.minFreq_lineEdit.setText(str(newValue))
5408  self.minFreqUnitsIndex = newIndex
5409 
5410 
5411  @pyqtSignature("int")
5412  ##
5413  #
5414  # Update lineEdit value when the units ComboBox is changed.
5415  # This function is automatically executed by the GUI when the user changes the maximum sweep frequency units ComboBox.
5416  #
5418  #newIndex = self.maxFreq_comboBox.currentIndex()
5419  newIndex = value
5420  if self.maxFreqUnitsIndex is not None: # If it is None, the dialog has just been created and we set units for the 1st time
5421  newValue = float(self.maxFreq_lineEdit.text()) * pow(10, 3*(self.maxFreqUnitsIndex-newIndex))
5422  self.maxFreq_lineEdit.setText(str(newValue))
5423  self.maxFreqUnitsIndex = newIndex
5424 
5425  ##@}
5426 
5427 # End class: ParamEditDlg #
5428 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
5429 
5430 # !!!!!!!!!!!!!!!!!!!!!!!#
5431 # Global functions: #
5432 
5433 ##
5434 #
5435 # Runs parameter validation, filters synthesis and results plots.
5436 # @param P = Parameters class instance containing the parameter values that will be used in the computation. May be modified during computation.
5437 # @param FT = FrequencyTransform class instance.
5438 # @param symmetrizeZeros = Automatically symmetrize Generalized Chebyshev zeros in Hz in order to compute a folded matrix with uniform Q in resonators.
5439 # Equivalent to answering "Yes" in the GUI when it asks the user if he wants to symmetrize Generalized Chebyshev zeros in Hz.
5440 # @return CP = CharPolys class instance, containing Characteristic Polynomials. Losses are included.
5441 # @return CM = CouplingMatrices class instance, containing Coupling Matrices. Losses are included.
5442 # @return SP = Sparameters class instance, containing frequency axis and [S] data. Losses are included.
5443 # @return done = True if Synthesis has been done without errors, False otherwise.
5444 #
5445 def runSynthesis(P, FT, symmetrizeZeros):
5446  CP, CM, SP = None, None, None
5447 
5448  try:
5449 
5450  # Compute characteristic polynomials
5451  CP = CharPolys(P, FT, symmetrizeZeros)
5452  myPrint("Relative error in the computation of lossless characteristic polynomials = %g" % CP.rootsErr)
5453 
5454  if P.synthTech.title() == u'Predistortion':
5455  if importedLossyFilters is False:
5456  if existingLossyFilters is False:
5457  raise synthError, """Pre-distorted characteristic polynomials are in the Library of Lossy Filters, which is not installed in this system.
5458  <p>Contact lossyfilters@tsc.upc.edu to get the Library of Lossy Filters.
5459  """
5460  else:
5461  raise synthError, """Importing the 'Library of Lossy Filters' failed. <br>
5462  You have a liblossyfilters.pyc file, but it cannot be imported. <br>
5463  Possibly liblossyfilters.pyc was compiled using a different Python version than yours.
5464  <p>Your installed Python version is: %s on %s
5465  <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5466  """ % (platform.python_version(), platform.system())
5467  elif importedCheckLicense is False:
5468  if existingCheckLicense is False:
5469  raise synthError, """There is no check_license.pyc file in your system.
5470  It should have been installed together with libextrafilters.pyc file,
5471  which actually is in your system.
5472  Please follow the 'Extra Filters Library' installation instructions.
5473  """
5474  else:
5475  raise synthError, """Importing the 'License Check Library' failed. <br>
5476  You have a libchecklicense.pyc file, but it cannot be imported. <br>
5477  Possibly libchecklicense.pyc was compiled using a different Python version than yours.
5478  <p>Your installed Python version is: %s on %s
5479  <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5480  """ % (platform.python_version(), platform.system())
5481  else:
5482  predistortion(P, CP, FT )
5483  myPrint("Relative error in the computation of pre-distorted characteristic polynomials = %g" % CP.rootsErr)
5484 
5485  elif P.synthTech.title() == u'Prescribed Insertion Loss':
5486  if importedLossyFilters is False:
5487  if existingLossyFilters is False:
5488  raise synthError, """Prescribed insertion loss characteristic polynomials are in the Library of Lossy Filters, which is not installed in this system.
5489  <p>Contact lossyfilters@tsc.upc.edu to get the Library of Lossy Filters.
5490  """
5491  else:
5492  raise synthError, """Importing the 'Library of Lossy Filters' failed. <br>
5493  You have a liblossyfilters.pyc file, but it cannot be imported. <br>
5494  Possibly liblossyfilters.pyc was compiled using a different Python version than yours.
5495  <p>Your installed Python version is: %s on %s
5496  <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5497  """ % (platform.python_version(), platform.system())
5498  elif importedCheckLicense is False:
5499  if existingCheckLicense is False:
5500  raise synthError, """There is no check_license.pyc file in your system.
5501  It should have been installed together with libextrafilters.pyc file,
5502  which actually is in your system.
5503  Please follow the 'Extra Filters Library' installation instructions.
5504  """
5505  else:
5506  raise synthError, """Importing the 'License Check Library' failed. <br>
5507  You have a libchecklicense.pyc file, but it cannot be imported. <br>
5508  Possibly libchecklicense.pyc was compiled using a different Python version than yours.
5509  <p>Your installed Python version is: %s on %s
5510  <p>Read the README.html documentation or contact lossyfilters@tsc.upc.edu for assistence.
5511  """ % (platform.python_version(), platform.system())
5512  else:
5513  nonUniformQ(P, CP)
5514 
5515  # Compute [S] parameters from characteristic polynomials
5516  SP = Sparameters(P, CP, FT)
5517 
5518  # Compute coupling matrix
5519  Yo = 1 # Normalized load
5520  CM = CouplingMatrices(P, CP, Yo, FT, SP)
5521  myPrint("Relative error in the computation of transversal coupling matrix = %g" % CM.CM_error)
5522 
5523  # Compute error between [S] from coupling matrices and [S] from characteristic polynomials
5524  # Do not compute it in the case 'kS21+pole/zero', since the node corresponding to the added pole is removed from the CM, resulting in large errors.
5525  if (P.synthTech.title() != u'Prescribed Insertion Loss') or (P.nuqTech != u'kS21+pole/zero'):
5526 # for MatQ in CM.listM:
5527  for MatQ in [ CM.listM[0] ]: # It is slow to compute [S] for all the MatQ, do it only for the 1st one (TCM) just to estimate the error in CM
5528  myPrint(MatQ.name)
5529  stErrorCM, CM_error = SP.fromCouplingMatrix(MatQ)
5530  myPrint(stErrorCM + ', Relative error: %e\n' % CM_error)
5531  CM.updateCM_error(CM_error, st = 'Error in %s' % MatQ.name)
5532 
5533  if P.outDir is not None:
5534  # Normalize and expand output directory name, just in case
5535  P.outDir = os.path.normpath(P.outDir)
5536  P.outDir = os.path.expanduser(P.outDir)
5537 
5538  # Check if directory exists
5539  if not os.path.isdir(P.outDir):
5540  P.outDirName = None
5541  warningMsg(u"WARNING: Directory '%s' does not exist. <p>Results not saved" % P.outDir)
5542  else:
5543  # Create output name using outDir and outName
5544  P.outDirName = os.path.join(P.outDir, P.outName)
5545  else:
5546  P.outDirName = P.outName
5547 
5548  if P.outDirName is not None:
5549  # Normalize output path just in case
5550  P.outDirName = os.path.normpath(P.outDirName)
5551 
5552  # Save everything
5553  CP.saveCharPolys(P, saveSignificantDigits)
5554  SP.saveSparam(P, saveSignificantDigits)
5555  if saveDefaultMatrices:
5556  CM.saveCouplingMatrices(P, SP, saveSignificantDigits, saveSignificantDigitsEnergy)
5557 
5558  except licenseError, err:
5559  criticalErrorMsg('License ERROR: <p>' + unicode(err))
5560  return None, None, None, False
5561 
5562  except synthError, err:
5563  criticalErrorMsg('<b>ERROR in Filter Synthesis:</b><p>' + unicode(err))
5564  return None, None, None, False
5565 
5566  return CP, CM, SP, True
5567 
5568 
5569 ##
5570 #
5571 # Print error message and command line help.
5572 # @param st = Error message string.
5573 # @param exit_code = program exit return code.
5574 #
5575 def usage(st, exit_code):
5576 
5577  pgrm_name = os.path.basename(sys.argv[0])
5578  print st
5579  print 'usage: ' + pgrm_name + ' [-vh] [--help] [parameter_file]'
5580  print '\nOptions:'
5581  print '-v: Verbose output. Show more information (for debugging) at the log in the main window (or at the console with --nogui flag).'
5582  print '-s: Automatically symmetrize Generalized Chebyshev zeros in Hz in order to compute a folded matrix with uniform Q in resonators.'
5583  print '-r: Automatically run Synthesis. Needs simultaneous parameter file specification.'
5584  print '-m: Automatically open Coupling Matrices window. Needs simultaneous -r flag.'
5585  print '-e: Automatically open Matrix Edit window. Needs simultaneous -m flag.'
5586  print '-p N: Save all default coupling matrices to file after they are computed, using N significant digits. By default the matrices must be saved manually using the GUI.'
5587  print '-h, --help: help'
5588  print '--nogui: Command line interface'
5589  print '--style windows, motif, cleanlooks, plastique or cde'
5590  print '\nArguments:'
5591  print 'parameter_file (optional): Path to ASCII file containing parameter values\n'
5592  sys.exit(exit_code)
5593 
5594 # End: Global functions: #
5595 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
5596 
5597 
5598 # !!!!!!!!!!!!!!!!!!!!!!!#
5599 # MAIN PROGRAM #
5600 
5601 if __name__ == '__main__':
5602  """
5603  Main program
5604  """
5605 
5606  ##
5607  # Program name as run by the user.
5608  applicationName = "Microwave filters GUI"
5609 
5610  # Check input arguments syntax
5611  try:
5612  opts, args = getopt.getopt(sys.argv[1:], "hsvrmep:", ["style=", "help", "nogui"])
5613  except getopt.GetoptError:
5614  # print help information and exit:
5615  usage('Error: Bad command line parameters', 2)
5616 
5617  QtArgs = list()
5618  QtArgs.append(sys.argv[0])
5619 
5620  # Initialize command line flags class
5621  symmetrizeZeros = None
5622  verbose = autoRun = autoMatrix = autoEditMatrix = False
5623  QtArgsStyle = None
5624  nogui = False
5625  saveDefaultMatrices = False
5626 
5627  for o, a in opts:
5628  if o == "-v":
5629  verbose = True
5630 
5631  if o == "-s":
5632  symmetrizeZeros = True
5633 
5634  if o == "-r":
5635  autoRun = True
5636 
5637  if o == "-m":
5638  autoMatrix = True
5639 
5640  if o == "-e":
5641  autoEditMatrix = True
5642 
5643  if o == "-p":
5644  try:
5645  saveSignificantDigits = int(a)
5646  except ValueError:
5647  print "ERROR in command line flags:"
5648  print "Precision flag -p must take a numeric argument."
5649  print "To get help use -h flag:\n%s -h\n" % sys.argv[0]
5650  sys.exit(0)
5651  saveDefaultMatrices = True
5652 
5653  if o in ("-h", "--help"):
5654  usage('Help:', 0)
5655 
5656  if o == "--nogui":
5657  nogui = True
5658 
5659  if o in ["--style"]:
5660  QtArgsStyle = a
5661 
5662  setGlobalVariablesLibCommonFunc(verbose = verbose, nogui = nogui)
5663 
5664  printHeader( applicationName, __doc__)
5665 
5666 # myPrint('Significant digits to use when saving coupling matrices: %d' % saveSignificantDigits)
5667 
5668  if nogui: # Command line interface
5669  if len(args) != 1: usage('Error: Incorrect number of arguments', 2)
5670 
5671  ##
5672  # Name of the parameters file to process
5673  fileName = args[0]
5674 
5675  ##
5676  # Parameters class instance containing filter and synthesis parameters read from the parameters file.
5677  P = readParamFile(fileName)
5678 
5679  ##
5680  # Create frequency transform instance
5682 
5683  # Execute the synthesis
5684  CP, CM, SP, done = runSynthesis(P, FT, symmetrizeZeros)
5685 
5686  else: # Graphical User Interface (GUI)
5687  if QtArgsStyle is None: QtArgsStyle = 'cleanlooks'
5688  QtArgs.extend(['-style', QtArgsStyle ])
5689 
5690  if len(args) > 1: usage('Error: Incorrect number of arguments', 2)
5691  else:
5692  if len(args) == 1: fileName = unicode(args[0])
5693  else: fileName = None # Start with blank parameter values
5694 
5695  ##
5696  # Create validator strings and regular expressions. The are global variables to be accessible from everywhere.
5697  stFloatPoint = r'((\d+\.?\d*|\d*\.?\d+)([Ee][-+]?\d+)?)'
5698  stReal = r'(([-+]?)' + stFloatPoint + '\s*)'
5699  stRealPositive = r'((\+?)' + stFloatPoint + '\s*)'
5700  stComplex = r'(([-+]?' + stFloatPoint + r'|[-+]?(' + stFloatPoint + r')?[ij]|[-+]?' + stFloatPoint + r'\s*[-+]\s*(' + stFloatPoint + r')?[ij])\s*)'
5701  stRealList = stReal + r'(,\s*' + stReal + r')*'
5702  stRealPositiveList = stRealPositive + r'(,\s*' + stRealPositive + r')*'
5703  stComplexList = stComplex + r'(,\s*' + stComplex + r')*'
5704 
5705  reReal = QRegExp( stReal + r'|^$')
5706  reRealPositive = QRegExp( stRealPositive + r'|^$')
5707  reRealPositiveInf = QRegExp( stRealPositive + r'|^$|inf|Inf')
5708  reComplex = QRegExp( stComplex + r'|^$')
5709  reRealList = QRegExp( stRealList + r'|^$' )
5710  reRealPositiveList = QRegExp( stRealPositiveList + r'|^$' )
5711  reComplexList = QRegExp( stComplexList + r'|^$' )
5712 
5713 
5714  # Start user interface
5715 
5716  #QApplication.setStyle(QCleanlooksStyle())
5717  #QApplication.setStyle(QWindowsXPStyle())
5718 
5719  ##
5720  # QApplication class instance, containing our application
5721  app = QApplication(QtArgs)
5722  app.setOrganizationName("UPC")
5723  app.setOrganizationDomain("http://www.tsc.upc.edu/hfs; http://www.tsc.upc.edu/antennalab")
5724  app.setApplicationName(applicationName)
5725  app.setWindowIcon(QIcon(":/icon/icons/icon_Spar-5.png"))
5726 
5727  mainWindow = MainWindow(fileName)
5728  mainWindow.show()
5729  #mainWindow.activateWindow()
5730  app.exec_()
5731 
5732 
5733